From ae64ee59318f46c939429655014efdc583342b14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Thu, 16 Jul 2026 15:43:01 +0400 Subject: [PATCH] =?UTF-8?q?resource-generator:=20SubParams=20nested=20fix?= =?UTF-8?q?=20(=D0=A1=D0=BE=D0=BD=D0=BD=D0=B5=D1=82)=20=E2=80=94=20AlignPa?= =?UTF-8?q?ramTypes=20recursive,=20HasNestedParams/NeedsStrings=20skip=20a?= =?UTF-8?q?rray-map-fixed,=20bump=205.0.74?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../internal/helpers/helpers.go | 23 +- .../internal/loader/loader.go | 29 +- .../internal/params/params.go | 55 ++ .../internal/templates/instance.go | 23 +- .../internal/types/types.go | 6 +- generated/dev/go/100_openwhisk_resource.go | 382 ++++++++++ .../dev/go/109_zones_v2_record_resource.go | 362 +++++++++ generated/dev/go/109_zones_v2_resource.go | 484 ++++++++++++ generated/dev/go/110_dnszone_resource.go | 495 +++++++++++++ generated/dev/go/111_dnsrecord_resource.go | 546 ++++++++++++++ generated/dev/go/112_tenant_resource.go | 372 ++++++++++ generated/dev/go/113_vc_complex_resource.go | 563 ++++++++++++++ .../dev/go/115_mariadb_database_resource.go | 343 +++++++++ generated/dev/go/115_mariadb_resource.go | 447 +++++++++++ generated/dev/go/115_mariadb_user_resource.go | 367 +++++++++ generated/dev/go/116_kafka_resource.go | 389 ++++++++++ generated/dev/go/116_kafka_topic_resource.go | 364 +++++++++ generated/dev/go/116_kafka_user_resource.go | 373 ++++++++++ generated/dev/go/119_akhq_resource.go | 554 ++++++++++++++ .../go/120_clickhouse_database_resource.go | 343 +++++++++ generated/dev/go/120_clickhouse_resource.go | 487 ++++++++++++ .../dev/go/120_clickhouse_user_resource.go | 359 +++++++++ generated/dev/go/12_s3_resource.go | 421 +++++++++++ generated/dev/go/12_s3_sub_user_resource.go | 358 +++++++++ generated/dev/go/13_s3bucket_resource.go | 478 ++++++++++++ .../148_vc_mgmt_sthutrval_cluster_resource.go | 419 +++++++++++ generated/dev/go/149_valo_tenant_resource.go | 431 +++++++++++ .../go/150_k8s_sthutrval_cluster_resource.go | 429 +++++++++++ ...150_k8s_sthutrval_cluster_user_resource.go | 343 +++++++++ .../dev/go/151_k8s_openbao_kv_resource.go | 343 +++++++++ .../dev/go/151_k8s_openbao_policy_resource.go | 355 +++++++++ generated/dev/go/151_k8s_openbao_resource.go | 468 ++++++++++++ .../go/151_k8s_openbao_secrets_resource.go | 368 +++++++++ generated/dev/go/153_nifi_resource.go | 474 ++++++++++++ generated/dev/go/163_llm_ai_resource.go | 345 +++++++++ .../dev/go/163_llm_ai_secrets_resource.go | 401 ++++++++++ generated/dev/go/163_llm_ai_team_resource.go | 365 +++++++++ generated/dev/go/175_k8s_go_resource.go | 422 +++++++++++ generated/dev/go/19_vc_org_resource.go | 391 ++++++++++ generated/dev/go/19_vc_org_user_resource.go | 343 +++++++++ generated/dev/go/1_dummy_resource.go | 582 +++++++++++++++ generated/dev/go/20_vc_org_saas_resource.go | 348 +++++++++ generated/dev/go/21_vc_vdc_resource.go | 573 ++++++++++++++ generated/dev/go/22_vc_nsxt_resource.go | 614 +++++++++++++++ generated/dev/go/23_vc_vm_resource.go | 581 +++++++++++++++ generated/dev/go/24_vc_nat_resource.go | 322 ++++++++ generated/dev/go/25_vcexternalip_resource.go | 604 +++++++++++++++ generated/dev/go/26_vapp_resource.go | 571 ++++++++++++++ generated/dev/go/27_vc_vm_v2_resource.go | 350 +++++++++ generated/dev/go/28_vc_vm_v3_resource.go | 697 ++++++++++++++++++ generated/dev/go/29_vc_vdc_group_resource.go | 449 +++++++++++ .../dev/go/29_vc_vdc_group_vdc_resource.go | 326 ++++++++ generated/dev/go/2_template_resource.go | 397 ++++++++++ generated/dev/go/32_vmpostgre_resource.go | 514 +++++++++++++ .../dev/go/50_nextcloud_backup_resource.go | 344 +++++++++ generated/dev/go/50_nextcloud_resource.go | 414 +++++++++++ generated/dev/go/81_superset_resource.go | 447 +++++++++++ generated/dev/go/82_harbor_resource.go | 482 ++++++++++++ generated/dev/go/86_k8s_velero_resource.go | 401 ++++++++++ generated/dev/go/87_k8svalkey_resource.go | 423 +++++++++++ .../dev/go/87_k8svalkey_user_resource.go | 358 +++++++++ .../dev/go/88_k8s_ziti_controller_resource.go | 431 +++++++++++ generated/dev/go/89_flask_resource.go | 449 +++++++++++ .../dev/go/90_postgres_backup_resource.go | 326 ++++++++ .../dev/go/90_postgres_database_resource.go | 358 +++++++++ generated/dev/go/90_postgres_resource.go | 499 +++++++++++++ generated/dev/go/90_postgres_user_resource.go | 367 +++++++++ generated/dev/go/91_redis_resource.go | 393 ++++++++++ generated/dev/go/92_mongodb_resource.go | 389 ++++++++++ generated/dev/go/92_mongodb_user_resource.go | 364 +++++++++ generated/dev/go/93_rabbitmq_resource.go | 474 ++++++++++++ generated/dev/go/94_lucee_resource.go | 449 +++++++++++ generated/dev/go/95_nodejs_resource.go | 449 +++++++++++ generated/dev/go/96_pgadmin_resource.go | 515 +++++++++++++ generated/dev/go/97_nodered_resource.go | 431 +++++++++++ generated/dev/go/98_http_resource.go | 509 +++++++++++++ generated/dev/go/99_gitea_resource.go | 579 +++++++++++++++ generated/dev/go/registry.go | 81 ++ generated/prod/go/100_openwhisk_resource.go | 355 +++++++++ .../prod/go/109_zones_v2_record_resource.go | 366 +++++++++ generated/prod/go/109_zones_v2_resource.go | 484 ++++++++++++ generated/prod/go/110_dnszone_resource.go | 495 +++++++++++++ generated/prod/go/111_dnsrecord_resource.go | 546 ++++++++++++++ generated/prod/go/112_tenant_resource.go | 372 ++++++++++ generated/prod/go/113_vc_complex_resource.go | 563 ++++++++++++++ .../prod/go/114_gitea_complex_resource.go | 322 ++++++++ .../prod/go/115_mariadb_database_resource.go | 343 +++++++++ generated/prod/go/115_mariadb_resource.go | 447 +++++++++++ .../prod/go/115_mariadb_user_resource.go | 367 +++++++++ generated/prod/go/116_kafka_resource.go | 389 ++++++++++ generated/prod/go/116_kafka_topic_resource.go | 364 +++++++++ generated/prod/go/116_kafka_user_resource.go | 373 ++++++++++ generated/prod/go/117_nifi_resource.go | 533 ++++++++++++++ generated/prod/go/119_akhq_resource.go | 544 ++++++++++++++ .../go/120_clickhouse_database_resource.go | 343 +++++++++ generated/prod/go/120_clickhouse_resource.go | 487 ++++++++++++ .../prod/go/120_clickhouse_user_resource.go | 359 +++++++++ generated/prod/go/12_s3_resource.go | 421 +++++++++++ generated/prod/go/12_s3_sub_user_resource.go | 358 +++++++++ generated/prod/go/13_s3bucket_resource.go | 478 ++++++++++++ .../148_vc_mgmt_sthutrval_cluster_resource.go | 419 +++++++++++ generated/prod/go/149_valo_tenant_resource.go | 431 +++++++++++ .../go/150_k8s_sthutrval_cluster_resource.go | 429 +++++++++++ ...150_k8s_sthutrval_cluster_user_resource.go | 343 +++++++++ generated/prod/go/163_llm_ai_resource.go | 345 +++++++++ .../prod/go/163_llm_ai_secrets_resource.go | 389 ++++++++++ generated/prod/go/163_llm_ai_team_resource.go | 365 +++++++++ generated/prod/go/19_vc_org_resource.go | 391 ++++++++++ generated/prod/go/19_vc_org_user_resource.go | 343 +++++++++ generated/prod/go/1_dummy_resource.go | 593 +++++++++++++++ generated/prod/go/20_vc_org_saas_resource.go | 348 +++++++++ generated/prod/go/21_vc_vdc_resource.go | 573 ++++++++++++++ generated/prod/go/22_vc_nsxt_resource.go | 614 +++++++++++++++ generated/prod/go/23_vc_vm_resource.go | 581 +++++++++++++++ generated/prod/go/25_vcexternalip_resource.go | 604 +++++++++++++++ generated/prod/go/26_vapp_resource.go | 571 ++++++++++++++ generated/prod/go/27_vc_vm_v2_resource.go | 350 +++++++++ generated/prod/go/28_vc_vm_v3_resource.go | 697 ++++++++++++++++++ generated/prod/go/29_vc_vdc_group_resource.go | 449 +++++++++++ .../prod/go/29_vc_vdc_group_vdc_resource.go | 326 ++++++++ generated/prod/go/2_template_resource.go | 397 ++++++++++ .../prod/go/50_nextcloud_backup_resource.go | 344 +++++++++ generated/prod/go/50_nextcloud_resource.go | 414 +++++++++++ generated/prod/go/81_superset_resource.go | 436 +++++++++++ generated/prod/go/82_harbor_resource.go | 458 ++++++++++++ generated/prod/go/86_k8s_velero_resource.go | 402 ++++++++++ .../go/88_k8s_ziti_controller_resource.go | 420 +++++++++++ generated/prod/go/89_flask_resource.go | 449 +++++++++++ .../prod/go/90_postgres_backup_resource.go | 326 ++++++++ .../prod/go/90_postgres_database_resource.go | 358 +++++++++ generated/prod/go/90_postgres_resource.go | 475 ++++++++++++ .../prod/go/90_postgres_user_resource.go | 358 +++++++++ generated/prod/go/91_redis_resource.go | 393 ++++++++++ generated/prod/go/92_mongodb_resource.go | 495 +++++++++++++ generated/prod/go/92_mongodb_user_resource.go | 364 +++++++++ generated/prod/go/93_rabbitmq_resource.go | 452 ++++++++++++ generated/prod/go/94_lucee_resource.go | 449 +++++++++++ generated/prod/go/95_nodejs_resource.go | 449 +++++++++++ generated/prod/go/96_pgadmin_resource.go | 504 +++++++++++++ generated/prod/go/97_nodered_resource.go | 420 +++++++++++ generated/prod/go/98_http_resource.go | 498 +++++++++++++ generated/prod/go/99_gitea_resource.go | 579 +++++++++++++++ generated/prod/go/registry.go | 73 ++ generated/test/go/100_openwhisk_resource.go | 322 ++++++++ .../test/go/109_zones_v2_record_resource.go | 366 +++++++++ generated/test/go/109_zones_v2_resource.go | 484 ++++++++++++ generated/test/go/110_dnszone_resource.go | 495 +++++++++++++ generated/test/go/111_dnsrecord_resource.go | 546 ++++++++++++++ generated/test/go/112_tenant_resource.go | 372 ++++++++++ generated/test/go/113_vc_complex_resource.go | 563 ++++++++++++++ .../test/go/114_gitea_complex_resource.go | 322 ++++++++ .../test/go/115_mariadb_database_resource.go | 343 +++++++++ generated/test/go/115_mariadb_resource.go | 447 +++++++++++ .../test/go/115_mariadb_user_resource.go | 367 +++++++++ generated/test/go/116_kafka_resource.go | 389 ++++++++++ generated/test/go/116_kafka_topic_resource.go | 364 +++++++++ generated/test/go/116_kafka_user_resource.go | 373 ++++++++++ generated/test/go/117_nifi_resource.go | 533 ++++++++++++++ generated/test/go/119_akhq_resource.go | 554 ++++++++++++++ .../go/120_clickhouse_database_resource.go | 343 +++++++++ generated/test/go/120_clickhouse_resource.go | 487 ++++++++++++ .../test/go/120_clickhouse_user_resource.go | 359 +++++++++ generated/test/go/12_s3_resource.go | 421 +++++++++++ generated/test/go/12_s3_sub_user_resource.go | 358 +++++++++ generated/test/go/13_s3bucket_resource.go | 478 ++++++++++++ .../148_vc_mgmt_sthutrval_cluster_resource.go | 419 +++++++++++ generated/test/go/149_valo_tenant_resource.go | 431 +++++++++++ .../go/150_k8s_sthutrval_cluster_resource.go | 429 +++++++++++ ...150_k8s_sthutrval_cluster_user_resource.go | 343 +++++++++ .../test/go/151_k8s_openbao_kv_resource.go | 343 +++++++++ .../go/151_k8s_openbao_policy_resource.go | 355 +++++++++ generated/test/go/151_k8s_openbao_resource.go | 468 ++++++++++++ .../go/151_k8s_openbao_secrets_resource.go | 368 +++++++++ generated/test/go/163_llm_ai_resource.go | 345 +++++++++ .../test/go/163_llm_ai_secrets_resource.go | 389 ++++++++++ generated/test/go/163_llm_ai_team_resource.go | 365 +++++++++ generated/test/go/19_vc_org_resource.go | 391 ++++++++++ generated/test/go/19_vc_org_user_resource.go | 343 +++++++++ generated/test/go/1_dummy_resource.go | 582 +++++++++++++++ generated/test/go/20_vc_org_saas_resource.go | 348 +++++++++ generated/test/go/21_vc_vdc_resource.go | 573 ++++++++++++++ generated/test/go/22_vc_nsxt_resource.go | 614 +++++++++++++++ generated/test/go/23_vc_vm_resource.go | 581 +++++++++++++++ generated/test/go/25_vcexternalip_resource.go | 604 +++++++++++++++ generated/test/go/26_vapp_resource.go | 571 ++++++++++++++ generated/test/go/27_vc_vm_v2_resource.go | 350 +++++++++ generated/test/go/28_vc_vm_v3_resource.go | 697 ++++++++++++++++++ generated/test/go/29_vc_vdc_group_resource.go | 449 +++++++++++ .../test/go/29_vc_vdc_group_vdc_resource.go | 326 ++++++++ generated/test/go/2_template_resource.go | 397 ++++++++++ generated/test/go/32_vmpostgre_resource.go | 322 ++++++++ .../test/go/50_nextcloud_backup_resource.go | 344 +++++++++ generated/test/go/50_nextcloud_resource.go | 414 +++++++++++ generated/test/go/81_superset_resource.go | 447 +++++++++++ generated/test/go/82_harbor_resource.go | 482 ++++++++++++ generated/test/go/86_k8s_velero_resource.go | 401 ++++++++++ .../go/88_k8s_ziti_controller_resource.go | 431 +++++++++++ generated/test/go/89_flask_resource.go | 449 +++++++++++ .../test/go/90_postgres_backup_resource.go | 326 ++++++++ .../test/go/90_postgres_database_resource.go | 358 +++++++++ generated/test/go/90_postgres_resource.go | 475 ++++++++++++ .../test/go/90_postgres_user_resource.go | 358 +++++++++ generated/test/go/91_redis_resource.go | 393 ++++++++++ generated/test/go/92_mongodb_resource.go | 389 ++++++++++ generated/test/go/92_mongodb_user_resource.go | 364 +++++++++ generated/test/go/93_rabbitmq_resource.go | 462 ++++++++++++ generated/test/go/94_lucee_resource.go | 449 +++++++++++ generated/test/go/95_nodejs_resource.go | 449 +++++++++++ generated/test/go/96_pgadmin_resource.go | 515 +++++++++++++ generated/test/go/97_nodered_resource.go | 431 +++++++++++ generated/test/go/98_http_resource.go | 509 +++++++++++++ generated/test/go/99_gitea_resource.go | 579 +++++++++++++++ generated/test/go/registry.go | 78 ++ provider/main.go | 2 +- 214 files changed, 89122 insertions(+), 35 deletions(-) create mode 100644 generated/dev/go/100_openwhisk_resource.go create mode 100644 generated/dev/go/109_zones_v2_record_resource.go create mode 100644 generated/dev/go/109_zones_v2_resource.go create mode 100644 generated/dev/go/110_dnszone_resource.go create mode 100644 generated/dev/go/111_dnsrecord_resource.go create mode 100644 generated/dev/go/112_tenant_resource.go create mode 100644 generated/dev/go/113_vc_complex_resource.go create mode 100644 generated/dev/go/115_mariadb_database_resource.go create mode 100644 generated/dev/go/115_mariadb_resource.go create mode 100644 generated/dev/go/115_mariadb_user_resource.go create mode 100644 generated/dev/go/116_kafka_resource.go create mode 100644 generated/dev/go/116_kafka_topic_resource.go create mode 100644 generated/dev/go/116_kafka_user_resource.go create mode 100644 generated/dev/go/119_akhq_resource.go create mode 100644 generated/dev/go/120_clickhouse_database_resource.go create mode 100644 generated/dev/go/120_clickhouse_resource.go create mode 100644 generated/dev/go/120_clickhouse_user_resource.go create mode 100644 generated/dev/go/12_s3_resource.go create mode 100644 generated/dev/go/12_s3_sub_user_resource.go create mode 100644 generated/dev/go/13_s3bucket_resource.go create mode 100644 generated/dev/go/148_vc_mgmt_sthutrval_cluster_resource.go create mode 100644 generated/dev/go/149_valo_tenant_resource.go create mode 100644 generated/dev/go/150_k8s_sthutrval_cluster_resource.go create mode 100644 generated/dev/go/150_k8s_sthutrval_cluster_user_resource.go create mode 100644 generated/dev/go/151_k8s_openbao_kv_resource.go create mode 100644 generated/dev/go/151_k8s_openbao_policy_resource.go create mode 100644 generated/dev/go/151_k8s_openbao_resource.go create mode 100644 generated/dev/go/151_k8s_openbao_secrets_resource.go create mode 100644 generated/dev/go/153_nifi_resource.go create mode 100644 generated/dev/go/163_llm_ai_resource.go create mode 100644 generated/dev/go/163_llm_ai_secrets_resource.go create mode 100644 generated/dev/go/163_llm_ai_team_resource.go create mode 100644 generated/dev/go/175_k8s_go_resource.go create mode 100644 generated/dev/go/19_vc_org_resource.go create mode 100644 generated/dev/go/19_vc_org_user_resource.go create mode 100644 generated/dev/go/1_dummy_resource.go create mode 100644 generated/dev/go/20_vc_org_saas_resource.go create mode 100644 generated/dev/go/21_vc_vdc_resource.go create mode 100644 generated/dev/go/22_vc_nsxt_resource.go create mode 100644 generated/dev/go/23_vc_vm_resource.go create mode 100644 generated/dev/go/24_vc_nat_resource.go create mode 100644 generated/dev/go/25_vcexternalip_resource.go create mode 100644 generated/dev/go/26_vapp_resource.go create mode 100644 generated/dev/go/27_vc_vm_v2_resource.go create mode 100644 generated/dev/go/28_vc_vm_v3_resource.go create mode 100644 generated/dev/go/29_vc_vdc_group_resource.go create mode 100644 generated/dev/go/29_vc_vdc_group_vdc_resource.go create mode 100644 generated/dev/go/2_template_resource.go create mode 100644 generated/dev/go/32_vmpostgre_resource.go create mode 100644 generated/dev/go/50_nextcloud_backup_resource.go create mode 100644 generated/dev/go/50_nextcloud_resource.go create mode 100644 generated/dev/go/81_superset_resource.go create mode 100644 generated/dev/go/82_harbor_resource.go create mode 100644 generated/dev/go/86_k8s_velero_resource.go create mode 100644 generated/dev/go/87_k8svalkey_resource.go create mode 100644 generated/dev/go/87_k8svalkey_user_resource.go create mode 100644 generated/dev/go/88_k8s_ziti_controller_resource.go create mode 100644 generated/dev/go/89_flask_resource.go create mode 100644 generated/dev/go/90_postgres_backup_resource.go create mode 100644 generated/dev/go/90_postgres_database_resource.go create mode 100644 generated/dev/go/90_postgres_resource.go create mode 100644 generated/dev/go/90_postgres_user_resource.go create mode 100644 generated/dev/go/91_redis_resource.go create mode 100644 generated/dev/go/92_mongodb_resource.go create mode 100644 generated/dev/go/92_mongodb_user_resource.go create mode 100644 generated/dev/go/93_rabbitmq_resource.go create mode 100644 generated/dev/go/94_lucee_resource.go create mode 100644 generated/dev/go/95_nodejs_resource.go create mode 100644 generated/dev/go/96_pgadmin_resource.go create mode 100644 generated/dev/go/97_nodered_resource.go create mode 100644 generated/dev/go/98_http_resource.go create mode 100644 generated/dev/go/99_gitea_resource.go create mode 100644 generated/dev/go/registry.go create mode 100644 generated/prod/go/100_openwhisk_resource.go create mode 100644 generated/prod/go/109_zones_v2_record_resource.go create mode 100644 generated/prod/go/109_zones_v2_resource.go create mode 100644 generated/prod/go/110_dnszone_resource.go create mode 100644 generated/prod/go/111_dnsrecord_resource.go create mode 100644 generated/prod/go/112_tenant_resource.go create mode 100644 generated/prod/go/113_vc_complex_resource.go create mode 100644 generated/prod/go/114_gitea_complex_resource.go create mode 100644 generated/prod/go/115_mariadb_database_resource.go create mode 100644 generated/prod/go/115_mariadb_resource.go create mode 100644 generated/prod/go/115_mariadb_user_resource.go create mode 100644 generated/prod/go/116_kafka_resource.go create mode 100644 generated/prod/go/116_kafka_topic_resource.go create mode 100644 generated/prod/go/116_kafka_user_resource.go create mode 100644 generated/prod/go/117_nifi_resource.go create mode 100644 generated/prod/go/119_akhq_resource.go create mode 100644 generated/prod/go/120_clickhouse_database_resource.go create mode 100644 generated/prod/go/120_clickhouse_resource.go create mode 100644 generated/prod/go/120_clickhouse_user_resource.go create mode 100644 generated/prod/go/12_s3_resource.go create mode 100644 generated/prod/go/12_s3_sub_user_resource.go create mode 100644 generated/prod/go/13_s3bucket_resource.go create mode 100644 generated/prod/go/148_vc_mgmt_sthutrval_cluster_resource.go create mode 100644 generated/prod/go/149_valo_tenant_resource.go create mode 100644 generated/prod/go/150_k8s_sthutrval_cluster_resource.go create mode 100644 generated/prod/go/150_k8s_sthutrval_cluster_user_resource.go create mode 100644 generated/prod/go/163_llm_ai_resource.go create mode 100644 generated/prod/go/163_llm_ai_secrets_resource.go create mode 100644 generated/prod/go/163_llm_ai_team_resource.go create mode 100644 generated/prod/go/19_vc_org_resource.go create mode 100644 generated/prod/go/19_vc_org_user_resource.go create mode 100644 generated/prod/go/1_dummy_resource.go create mode 100644 generated/prod/go/20_vc_org_saas_resource.go create mode 100644 generated/prod/go/21_vc_vdc_resource.go create mode 100644 generated/prod/go/22_vc_nsxt_resource.go create mode 100644 generated/prod/go/23_vc_vm_resource.go create mode 100644 generated/prod/go/25_vcexternalip_resource.go create mode 100644 generated/prod/go/26_vapp_resource.go create mode 100644 generated/prod/go/27_vc_vm_v2_resource.go create mode 100644 generated/prod/go/28_vc_vm_v3_resource.go create mode 100644 generated/prod/go/29_vc_vdc_group_resource.go create mode 100644 generated/prod/go/29_vc_vdc_group_vdc_resource.go create mode 100644 generated/prod/go/2_template_resource.go create mode 100644 generated/prod/go/50_nextcloud_backup_resource.go create mode 100644 generated/prod/go/50_nextcloud_resource.go create mode 100644 generated/prod/go/81_superset_resource.go create mode 100644 generated/prod/go/82_harbor_resource.go create mode 100644 generated/prod/go/86_k8s_velero_resource.go create mode 100644 generated/prod/go/88_k8s_ziti_controller_resource.go create mode 100644 generated/prod/go/89_flask_resource.go create mode 100644 generated/prod/go/90_postgres_backup_resource.go create mode 100644 generated/prod/go/90_postgres_database_resource.go create mode 100644 generated/prod/go/90_postgres_resource.go create mode 100644 generated/prod/go/90_postgres_user_resource.go create mode 100644 generated/prod/go/91_redis_resource.go create mode 100644 generated/prod/go/92_mongodb_resource.go create mode 100644 generated/prod/go/92_mongodb_user_resource.go create mode 100644 generated/prod/go/93_rabbitmq_resource.go create mode 100644 generated/prod/go/94_lucee_resource.go create mode 100644 generated/prod/go/95_nodejs_resource.go create mode 100644 generated/prod/go/96_pgadmin_resource.go create mode 100644 generated/prod/go/97_nodered_resource.go create mode 100644 generated/prod/go/98_http_resource.go create mode 100644 generated/prod/go/99_gitea_resource.go create mode 100644 generated/prod/go/registry.go create mode 100644 generated/test/go/100_openwhisk_resource.go create mode 100644 generated/test/go/109_zones_v2_record_resource.go create mode 100644 generated/test/go/109_zones_v2_resource.go create mode 100644 generated/test/go/110_dnszone_resource.go create mode 100644 generated/test/go/111_dnsrecord_resource.go create mode 100644 generated/test/go/112_tenant_resource.go create mode 100644 generated/test/go/113_vc_complex_resource.go create mode 100644 generated/test/go/114_gitea_complex_resource.go create mode 100644 generated/test/go/115_mariadb_database_resource.go create mode 100644 generated/test/go/115_mariadb_resource.go create mode 100644 generated/test/go/115_mariadb_user_resource.go create mode 100644 generated/test/go/116_kafka_resource.go create mode 100644 generated/test/go/116_kafka_topic_resource.go create mode 100644 generated/test/go/116_kafka_user_resource.go create mode 100644 generated/test/go/117_nifi_resource.go create mode 100644 generated/test/go/119_akhq_resource.go create mode 100644 generated/test/go/120_clickhouse_database_resource.go create mode 100644 generated/test/go/120_clickhouse_resource.go create mode 100644 generated/test/go/120_clickhouse_user_resource.go create mode 100644 generated/test/go/12_s3_resource.go create mode 100644 generated/test/go/12_s3_sub_user_resource.go create mode 100644 generated/test/go/13_s3bucket_resource.go create mode 100644 generated/test/go/148_vc_mgmt_sthutrval_cluster_resource.go create mode 100644 generated/test/go/149_valo_tenant_resource.go create mode 100644 generated/test/go/150_k8s_sthutrval_cluster_resource.go create mode 100644 generated/test/go/150_k8s_sthutrval_cluster_user_resource.go create mode 100644 generated/test/go/151_k8s_openbao_kv_resource.go create mode 100644 generated/test/go/151_k8s_openbao_policy_resource.go create mode 100644 generated/test/go/151_k8s_openbao_resource.go create mode 100644 generated/test/go/151_k8s_openbao_secrets_resource.go create mode 100644 generated/test/go/163_llm_ai_resource.go create mode 100644 generated/test/go/163_llm_ai_secrets_resource.go create mode 100644 generated/test/go/163_llm_ai_team_resource.go create mode 100644 generated/test/go/19_vc_org_resource.go create mode 100644 generated/test/go/19_vc_org_user_resource.go create mode 100644 generated/test/go/1_dummy_resource.go create mode 100644 generated/test/go/20_vc_org_saas_resource.go create mode 100644 generated/test/go/21_vc_vdc_resource.go create mode 100644 generated/test/go/22_vc_nsxt_resource.go create mode 100644 generated/test/go/23_vc_vm_resource.go create mode 100644 generated/test/go/25_vcexternalip_resource.go create mode 100644 generated/test/go/26_vapp_resource.go create mode 100644 generated/test/go/27_vc_vm_v2_resource.go create mode 100644 generated/test/go/28_vc_vm_v3_resource.go create mode 100644 generated/test/go/29_vc_vdc_group_resource.go create mode 100644 generated/test/go/29_vc_vdc_group_vdc_resource.go create mode 100644 generated/test/go/2_template_resource.go create mode 100644 generated/test/go/32_vmpostgre_resource.go create mode 100644 generated/test/go/50_nextcloud_backup_resource.go create mode 100644 generated/test/go/50_nextcloud_resource.go create mode 100644 generated/test/go/81_superset_resource.go create mode 100644 generated/test/go/82_harbor_resource.go create mode 100644 generated/test/go/86_k8s_velero_resource.go create mode 100644 generated/test/go/88_k8s_ziti_controller_resource.go create mode 100644 generated/test/go/89_flask_resource.go create mode 100644 generated/test/go/90_postgres_backup_resource.go create mode 100644 generated/test/go/90_postgres_database_resource.go create mode 100644 generated/test/go/90_postgres_resource.go create mode 100644 generated/test/go/90_postgres_user_resource.go create mode 100644 generated/test/go/91_redis_resource.go create mode 100644 generated/test/go/92_mongodb_resource.go create mode 100644 generated/test/go/92_mongodb_user_resource.go create mode 100644 generated/test/go/93_rabbitmq_resource.go create mode 100644 generated/test/go/94_lucee_resource.go create mode 100644 generated/test/go/95_nodejs_resource.go create mode 100644 generated/test/go/96_pgadmin_resource.go create mode 100644 generated/test/go/97_nodered_resource.go create mode 100644 generated/test/go/98_http_resource.go create mode 100644 generated/test/go/99_gitea_resource.go create mode 100644 generated/test/go/registry.go diff --git a/TOOLS/resource-generator/internal/helpers/helpers.go b/TOOLS/resource-generator/internal/helpers/helpers.go index 4d131dc..e3abf12 100644 --- a/TOOLS/resource-generator/internal/helpers/helpers.go +++ b/TOOLS/resource-generator/internal/helpers/helpers.go @@ -193,14 +193,15 @@ func PlanModifierExpr(p types.Param) string { // ─── Nested (map-fixed) helpers ────────────────────────────────────────────── -// IsNested возвращает true для параметров с SubParams (map-fixed/array-map-fixed). +// IsNested возвращает true для параметров с SubParams (map-fixed). +// array-map-fixed исключается — требует отдельной обработки. func IsNested(p types.Param) bool { - return p.IsNested + return p.HasSubParams && !IsNestedList(p) } // IsNestedList возвращает true для array-map-fixed (список объектов). func IsNestedList(p types.Param) bool { - return p.IsNested && strings.Contains(strings.ToLower(p.Type), "array") + return p.HasSubParams && strings.Contains(strings.ToLower(p.Type), "array") } // NestedModelName возвращает имя вложенного Go-struct для ресурса и параметра. @@ -245,12 +246,13 @@ func NestedSchemaBlock(p types.Param) string { } // NestedSchemaEnd возвращает закрывающую строку для nested-атрибута. +// Закрывает Attributes map и сам атрибут: `}},` для SingleNested, `}}},` для ListNested. func NestedSchemaEnd(p types.Param) string { t := NestedSchemaType(p) if t == "ListNested" { - return "}}," + return "}}}," } - return "}," + return "}}," } // SubSchemaType возвращает тип схемы для подпараметра. @@ -295,23 +297,20 @@ func SubParamFormat(p types.Param, varPrefix string) string { } // NestedJSONExpr возвращает выражение для сборки JSON из nested struct через resources_core.BuildJSON. -// BuildJSON принимает map[string]string где ключи — имена полей, -// значения — уже отформатированные строки (числа без кавычек, строки с кавычками). -// Пример: resources_core.BuildJSON(map[string]string{"cpu": "500", "name": "\"hello\""}) → {"cpu":500,"name":"hello"} -func NestedJSONExpr(p types.Param, varName string) string { +// varPrefix — префикс переменной (например "data" или "plan"). +// Генерирует доступ: data.ClusterConfiguration.Cpu, data.ClusterConfiguration.Memory, ... +func NestedJSONExpr(p types.Param, varPrefix string) string { + varName := varPrefix + "." + ToCamel(p.Code) pairs := make([]string, 0, len(p.SubParams)) for _, sp := range p.SubParams { field := varName + "." + ToCamel(sp.Code) var valExpr string switch strings.ToLower(sp.Type) { case "bool": - // BuildJSON ожидает "true"/"false" (без кавычек) valExpr = fmt.Sprintf(`fmt.Sprintf("%%v", %s.ValueBool())`, field) case "int64", "int", "number": - // BuildJSON ожидает "500" (без кавычек) valExpr = fmt.Sprintf(`fmt.Sprintf("%%d", %s.ValueInt64())`, field) default: - // BuildJSON ожидает "\"hello\"" (с кавычками) valExpr = fmt.Sprintf(`fmt.Sprintf("\"%%s\"", %s.ValueString())`, field) } pairs = append(pairs, fmt.Sprintf(`"%s": %s`, sp.Code, valExpr)) diff --git a/TOOLS/resource-generator/internal/loader/loader.go b/TOOLS/resource-generator/internal/loader/loader.go index 5414f8b..ed2e4cb 100644 --- a/TOOLS/resource-generator/internal/loader/loader.go +++ b/TOOLS/resource-generator/internal/loader/loader.go @@ -104,8 +104,13 @@ func LoadSpecs(dir string) ([]types.GenResource, []types.GenSubresource, []types HasDomainParam: hasDomainParam, } params.Analyze(&gr.UsesBool, &gr.UsesInt64, &gr.UsesString, &gr.HasDefaults, &gr.NeedsBoolDefault, &gr.NeedsInt64Default, &gr.NeedsStringDefault, gr.SchemaParams) + // Анализируем nested sub-params для default-импортов + if params.AnalyzeNestedDefaults(&gr.NeedsBoolDefault, &gr.NeedsInt64Default, &gr.NeedsStringDefault, gr.SchemaParams) { + gr.NeedsFmtImport = true + } gr.NeedsJsonPlanMod = params.AnalyzeJsonPlanMod(gr.SchemaParams) gr.NeedsStringsImport = params.HasRestoreCasingParams(gr.SchemaParams) || params.AnalyzeNeedsStrings(gr.ModifyParams) || params.AnalyzeNeedsStrings(gr.CreateOnlyRequiredParams) || params.AnalyzeNeedsStrings(gr.CreateOnlyParams) + gr.NeedsFmtImport = params.HasNestedParams(gr.SchemaParams) // --- Action processing (ДО append, чтобы HasRedeploy попал в слайс) --- for _, op := range spec.Operations { @@ -239,18 +244,18 @@ func ConvertParams(params []types.ParamSpec) []types.Param { } } out = append(out, types.Param{ - ID: p.ID, - Code: p.Code, - Type: typeName, - Required: p.Required, - Default: defVal, - RefSvcId: ref, - Descr: strings.TrimSpace(p.Descr), - Man: strings.TrimSpace(p.Man), - Sensitive: p.IsSensitive, - IsJson: strings.EqualFold(strings.TrimSpace(p.DataType), "json"), - IsNested: isNested, - SubParams: subParams, + ID: p.ID, + Code: p.Code, + Type: typeName, + Required: p.Required, + Default: defVal, + RefSvcId: ref, + Descr: strings.TrimSpace(p.Descr), + Man: strings.TrimSpace(p.Man), + Sensitive: p.IsSensitive, + IsJson: strings.EqualFold(strings.TrimSpace(p.DataType), "json"), + HasSubParams: isNested, + SubParams: subParams, }) } return out diff --git a/TOOLS/resource-generator/internal/params/params.go b/TOOLS/resource-generator/internal/params/params.go index 58b88d4..5461d49 100644 --- a/TOOLS/resource-generator/internal/params/params.go +++ b/TOOLS/resource-generator/internal/params/params.go @@ -66,6 +66,21 @@ func AlignParamTypes(params []types.Param, schema []types.Param) []types.Param { } } + // Рекурсивно выравниваем SubParams (например cpu может быть string в modify, но int64 в create). + schemaByCode := make(map[string]types.Param, len(schema)) + for _, p := range schema { + schemaByCode[strings.ToLower(strings.TrimSpace(p.Code))] = p + } + for i, p := range params { + if !p.HasSubParams { + continue + } + key := strings.ToLower(strings.TrimSpace(p.Code)) + if sp, ok := schemaByCode[key]; ok && len(sp.SubParams) > 0 { + params[i].SubParams = AlignParamTypes(p.SubParams, sp.SubParams) + } + } + return params } @@ -231,6 +246,42 @@ func AnalyzeJsonPlanMod(params []types.Param) bool { return false } +// HasNestedParams возвращает true, если есть nested (только map-fixed, не array-map-fixed) параметры. +func HasNestedParams(params []types.Param) bool { + for _, p := range params { + if p.HasSubParams && !strings.Contains(strings.ToLower(p.Type), "array") { + return true + } + } + return false +} + +// AnalyzeNestedDefaults проверяет sub-params nested-параметров на наличие default'ов. +// Возвращает true если найдены nested параметры. +func AnalyzeNestedDefaults(needsBoolDefault *bool, needsInt64Default *bool, needsStringDefault *bool, params []types.Param) bool { + found := false + for _, p := range params { + if len(p.SubParams) == 0 { + continue + } + found = true + for _, sp := range p.SubParams { + if sp.Default == "" { + continue + } + switch strings.ToLower(sp.Type) { + case "bool": + *needsBoolDefault = true + case "int64", "int", "number": + *needsInt64Default = true + default: + *needsStringDefault = true + } + } + } + return found +} + // HasRestoreCasingParams возвращает true, если есть ref_svc-параметры (кроме S3). func HasRestoreCasingParams(params []types.Param) bool { for _, p := range params { @@ -244,6 +295,10 @@ func HasRestoreCasingParams(params []types.Param) bool { // AnalyzeNeedsStrings возвращает true, если есть строковые параметры. func AnalyzeNeedsStrings(params []types.Param) bool { for _, p := range params { + // Пропускаем IsNested (map-fixed без array): они не участвуют в strings.EqualFold + if p.HasSubParams && !strings.Contains(strings.ToLower(p.Type), "array") { + continue + } t := strings.ToLower(strings.TrimSpace(p.Type)) if t != "bool" && t != "int" && t != "int64" && t != "number" { return true diff --git a/TOOLS/resource-generator/internal/templates/instance.go b/TOOLS/resource-generator/internal/templates/instance.go index 2b6d2f3..d66d30a 100644 --- a/TOOLS/resource-generator/internal/templates/instance.go +++ b/TOOLS/resource-generator/internal/templates/instance.go @@ -7,6 +7,9 @@ import ( {{- if .NeedsStringsImport }} "strings" {{- end }} + {{- if .NeedsFmtImport }} + "fmt" + {{- end }} "terraform-provider-nubes/internal/core" "terraform-provider-nubes/internal/resources_core" @@ -39,7 +42,7 @@ type {{ToCamel .Name}}Resource struct { } {{- range .SchemaParams }} -{{- if .IsNested }} +{{- if (IsNested .) }} // {{NestedModelName $.Name .Code}} — вложенная модель для map-fixed параметра {{.Code}}. type {{NestedModelName $.Name .Code}} struct { {{- range .SubParams }} @@ -61,7 +64,7 @@ type {{ToCamel .Name}}Model struct { GitRevision types.String ` + "`" + `tfsdk:"git_revision"` + "`" + ` {{- end }} {{- range .SchemaParams }} - {{- if .IsNested }} + {{- if (IsNested .) }} {{ToCamel .Code}} {{NestedTfType . $.Name}} ` + "`" + `tfsdk:"{{ToSnake .Code}}"` + "`" + ` {{- else }} {{ToCamel .Code}} {{ParamType .}} ` + "`" + `tfsdk:"{{ToSnake .Code}}"` + "`" + ` @@ -89,7 +92,7 @@ func (r *{{ToCamel .Name}}Resource) Schema(ctx context.Context, req resource.Sch "operation_timeout": schema.StringAttribute{Optional: true}, "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, {{- range .SchemaParams }} - {{- if .IsNested }} + {{- if (IsNested .) }} "{{ToSnake .Code}}": {{NestedSchemaBlock .}} {{- range .SubParams }} "{{ToSnake .Code}}": schema.{{SubSchemaType .}}Attribute{ @@ -171,6 +174,7 @@ func (r *{{ToCamel .Name}}Resource) ModifyPlan(ctx context.Context, req resource } } {{- range .CreateOnlyParams }} + {{- if not (IsNested .) }} if !plan.{{ToCamel .Code}}.IsNull() && !plan.{{ToCamel .Code}}.IsUnknown() && !state.{{ToCamel .Code}}.IsNull() && !state.{{ToCamel .Code}}.IsUnknown() { {{- if eq (ParamType .) "types.Bool" }} if plan.{{ToCamel .Code}}.ValueBool() != state.{{ToCamel .Code}}.ValueBool() { @@ -193,10 +197,11 @@ func (r *{{ToCamel .Name}}Resource) ModifyPlan(ctx context.Context, req resource {{- end }} } {{- end }} + {{- end }} return } {{- range .SchemaParams }} - {{- if and .Required (eq (ParamDefaultExpr .) "") }} + {{- if and .Required (eq (ParamDefaultExpr .) "") (not (IsNested .)) }} if config.{{ToCamel .Code}}.IsNull() { resp.Diagnostics.AddError("Missing required attribute", "{{ToSnake .Code}} is required.") return @@ -230,7 +235,7 @@ func (r *{{ToCamel .Name}}Resource) ModifyPlan(ctx context.Context, req resource {{- end }} params := map[int]string{ {{- range .CreateParams }} - {{- if .IsNested }} + {{- if (IsNested .) }} {{.ID}}: {{NestedJSONExpr . "config"}}, {{- else }} {{.ID}}: {{ParamFormat . (printf "config.%s" (ToCamel .Code))}}, @@ -255,7 +260,7 @@ func (r *{{ToCamel .Name}}Resource) Create(ctx context.Context, req resource.Cre return } {{- range .SchemaParams }} - {{- if and .Required (eq (ParamDefaultExpr .) "") }} + {{- if and .Required (eq (ParamDefaultExpr .) "") (not (IsNested .)) }} if data.{{ToCamel .Code}}.IsNull() || data.{{ToCamel .Code}}.IsUnknown() { resp.Diagnostics.AddError("Missing required attribute", "{{ToSnake .Code}} is required.") return @@ -320,7 +325,7 @@ func (r *{{ToCamel .Name}}Resource) Create(ctx context.Context, req resource.Cre params := map[int]string{ {{- range .CreateParams }} - {{- if .IsNested }} + {{- if (IsNested .) }} {{.ID}}: {{NestedJSONExpr . "data"}}, {{- else }} {{.ID}}: {{ParamFormat . (printf "data.%s" (ToCamel .Code))}}, @@ -477,6 +482,7 @@ func (r *{{ToCamel .Name}}Resource) Update(ctx context.Context, req resource.Upd hasServiceParamChanges := false {{- range .ModifyParams }} + {{- if not (IsNested .) }} if !hasServiceParamChanges { if plan.{{ToCamel .Code}}.IsNull() != state.{{ToCamel .Code}}.IsNull() || plan.{{ToCamel .Code}}.IsUnknown() != state.{{ToCamel .Code}}.IsUnknown() { hasServiceParamChanges = true @@ -497,6 +503,7 @@ func (r *{{ToCamel .Name}}Resource) Update(ctx context.Context, req resource.Upd } } {{- end }} + {{- end }} {{- if .HasRedeploy }} // --- Redeploy support (ARCHITECTURE.md) --- @@ -540,7 +547,7 @@ func (r *{{ToCamel .Name}}Resource) Update(ctx context.Context, req resource.Upd {{- end }} params := map[int]string{ {{- range .ModifyParams }} - {{- if .IsNested }} + {{- if (IsNested .) }} {{.ID}}: {{NestedJSONExpr . "plan"}}, {{- else }} {{.ID}}: {{ParamFormat . (printf "plan.%s" (ToCamel .Code))}}, diff --git a/TOOLS/resource-generator/internal/types/types.go b/TOOLS/resource-generator/internal/types/types.go index a85fbe6..4942c32 100644 --- a/TOOLS/resource-generator/internal/types/types.go +++ b/TOOLS/resource-generator/internal/types/types.go @@ -53,8 +53,8 @@ type Param struct { // При IsJson=true в схему добавляется JsonNormalize план-модификатор, // чтобы план и API-ответ (компактный JSON) всегда совпадали. IsJson bool - // IsNested — параметр типа map-fixed/array-map-fixed с подполями. - IsNested bool + // HasSubParams — параметр имеет подполя (map-fixed или array-map-fixed). + HasSubParams bool // SubParams — подполя для map-fixed/array-map-fixed параметров. SubParams []Param } @@ -93,6 +93,8 @@ type GenResource struct { NeedsJsonPlanMod bool // NeedsStringsImport: есть строковые ModifyParams (нужен EqualFold в Update). NeedsStringsImport bool + // NeedsFmtImport: есть nested (map-fixed) параметры, нужен fmt.Sprintf. + NeedsFmtImport bool } // GenSubresource — подресурс (nubes_{service}_{sub}). diff --git a/generated/dev/go/100_openwhisk_resource.go b/generated/dev/go/100_openwhisk_resource.go new file mode 100644 index 0000000..c4cbf39 --- /dev/null +++ b/generated/dev/go/100_openwhisk_resource.go @@ -0,0 +1,382 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: openwhisk +// Service ID: 100 + +var _ resource.Resource = &OpenwhiskResource{} +var _ resource.ResourceWithModifyPlan = &OpenwhiskResource{} +var _ resource.ResourceWithImportState = &OpenwhiskResource{} + +type OpenwhiskResource struct { + client *core.UniversalClient +} + +type OpenwhiskModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + ResourceRealm types.String `tfsdk:"resource_realm"` + Username types.String `tfsdk:"username"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewOpenwhiskResource() resource.Resource { + return &OpenwhiskResource{} +} + +func (r *OpenwhiskResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_openwhisk" +} + +func (r *OpenwhiskResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + "username": schema.StringAttribute{Required: true, MarkdownDescription: "Имя пользователя Openwhisk Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *OpenwhiskResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *OpenwhiskModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *OpenwhiskModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan OpenwhiskModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.Username.IsNull() && !plan.Username.IsUnknown() && !state.Username.IsNull() && !state.Username.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Username.ValueString(), state.Username.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить username", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.ResourceRealm.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + return + } + if config.ResourceRealm.IsUnknown() { + return + } + if config.Username.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "username is required.") + return + } + if config.Username.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 64: resources_core.FormatString(config.Username), + 281: resources_core.FormatString(config.ResourceRealm), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 100, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *OpenwhiskResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data OpenwhiskModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + return + } + if data.Username.IsNull() || data.Username.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "username is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 100, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 64: resources_core.FormatString(data.Username), + 281: resources_core.FormatString(data.ResourceRealm), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 100, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 100, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "username", Field: "Username", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *OpenwhiskResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state OpenwhiskModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 100, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "username", Field: "Username", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *OpenwhiskResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan OpenwhiskModel + var state OpenwhiskModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 100, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "username", Field: "Username", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *OpenwhiskResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state OpenwhiskModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *OpenwhiskResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *OpenwhiskResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/109_zones_v2_record_resource.go b/generated/dev/go/109_zones_v2_record_resource.go new file mode 100644 index 0000000..9cc2073 --- /dev/null +++ b/generated/dev/go/109_zones_v2_record_resource.go @@ -0,0 +1,362 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: zones_v2 +// Subresource: record + +var _ resource.Resource = &ZonesV2RecordResource{} + +// ZonesV2RecordResource manages a subresource via operations. +type ZonesV2RecordResource struct { + client *core.UniversalClient +} + +type ZonesV2RecordModel struct { + ID types.String `tfsdk:"id"` + ZonesV2ID types.String `tfsdk:"zones_v2_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + RecordInput types.String `tfsdk:"record_input"` + RecordName types.String `tfsdk:"record_name"` + RecordTTL types.Int64 `tfsdk:"record_t_t_l"` + RecordType types.String `tfsdk:"record_type"` +} + +func NewZonesV2RecordResource() resource.Resource { + return &ZonesV2RecordResource{} +} + +func (r *ZonesV2RecordResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_zones_v2_record" +} + +func (r *ZonesV2RecordResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "zones_v2_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "record_input": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Валидация производится только после запуска операции. Если при указанном типе будет указано некорректно значение, это будет описано в логе ошибки операции"}, + "record_name": schema.StringAttribute{Required: true, MarkdownDescription: "Имя зоны в конце проставляется автоматически Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "record_t_t_l": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(3600), MarkdownDescription: "Time-to-Leave записи. Раз в какое время запись будет обновлена"}, + "record_type": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("A")}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *ZonesV2RecordResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan ZonesV2RecordModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if plan.RecordName.IsNull() || plan.RecordName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "record_name is required.") + return + } + + instanceUID := strings.TrimSpace(plan.ZonesV2ID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "recordName": resources_core.FormatString(plan.RecordName), + } + identityCodes := []string{ + "recordName", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("record", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "record", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "recordName": resources_core.FormatString(plan.RecordName), + "recordTTL": resources_core.FormatInt64(plan.RecordTTL), + "recordType": resources_core.FormatString(plan.RecordType), + "recordInput": resources_core.FormatString(plan.RecordInput), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_record", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_record", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "record", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "record", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "record", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *ZonesV2RecordResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state ZonesV2RecordModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *ZonesV2RecordResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan ZonesV2RecordModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "modify_record" == "" { + var state ZonesV2RecordModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.ZonesV2ID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{ + "recordName": resources_core.FormatString(plan.RecordName), + "recordTTL": resources_core.FormatInt64(plan.RecordTTL), + "recordType": resources_core.FormatString(plan.RecordType), + "recordInput": resources_core.FormatString(plan.RecordInput), + }) + + idParams := map[string]string{ + "recordName": resources_core.FormatString(plan.RecordName), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "modify_record", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "record", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *ZonesV2RecordResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state ZonesV2RecordModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_record" == "" { + return + } + + instanceUID := strings.TrimSpace(state.ZonesV2ID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "recordName": resources_core.FormatString(state.RecordName), + } + identityCodes := []string{ + "recordName", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("record", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "recordName": resources_core.FormatString(state.RecordName), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_record", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *ZonesV2RecordResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/109_zones_v2_resource.go b/generated/dev/go/109_zones_v2_resource.go new file mode 100644 index 0000000..f07aab8 --- /dev/null +++ b/generated/dev/go/109_zones_v2_resource.go @@ -0,0 +1,484 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: zones_v2 +// Service ID: 109 + +var _ resource.Resource = &ZonesV2Resource{} +var _ resource.ResourceWithModifyPlan = &ZonesV2Resource{} +var _ resource.ResourceWithImportState = &ZonesV2Resource{} + +type ZonesV2Resource struct { + client *core.UniversalClient +} + +type ZonesV2Model struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + Records types.String `tfsdk:"records"` + ResourceRealm types.String `tfsdk:"resource_realm"` + ZoneExpire types.Int64 `tfsdk:"zone_expire"` + ZoneKind types.String `tfsdk:"zone_kind"` + ZoneMastersForSlave types.String `tfsdk:"zone_masters_for_slave"` + ZoneName types.String `tfsdk:"zone_name"` + ZoneRefresh types.Int64 `tfsdk:"zone_refresh"` + ZoneRetry types.Int64 `tfsdk:"zone_retry"` + ZoneTTL types.Int64 `tfsdk:"zone_t_t_l"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewZonesV2Resource() resource.Resource { + return &ZonesV2Resource{} +} + +func (r *ZonesV2Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_zones_v2" +} + +func (r *ZonesV2Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "records": schema.StringAttribute{Required: true}, + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("powerdns"), MarkdownDescription: "Не изменяется после создания."}, + "zone_expire": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(3600000), MarkdownDescription: "Время в течение которого вторичный DNS будет пытаться завершить синхронизацию зоны с первичным Не изменяется после создания."}, + "zone_kind": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("NATIVE"), MarkdownDescription: "Тип зоны Не изменяется после создания."}, + "zone_masters_for_slave": schema.StringAttribute{Optional: true, MarkdownDescription: "Использовать если выбран тип SLAVE. IP адреса в формате через пробел для настройки Master-серверов Не изменяется после создания."}, + "zone_name": schema.StringAttribute{Required: true, MarkdownDescription: "Имя зоны Не изменяется после создания."}, + "zone_refresh": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(86400), MarkdownDescription: "Время ожидания ответа вторичного DNS перед запросом SOA-записи с первичных серверов Не изменяется после создания."}, + "zone_retry": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(7200), MarkdownDescription: "Время в течение которого сервер повторно попробует синхронизировать информацию с первичных DNS-серверов Не изменяется после создания."}, + "zone_t_t_l": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(86400), MarkdownDescription: "Время, в течение которого информация будет кешироваться другими DNS-серверами Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *ZonesV2Resource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *ZonesV2Model + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *ZonesV2Model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan ZonesV2Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneExpire.IsNull() && !plan.ZoneExpire.IsUnknown() && !state.ZoneExpire.IsNull() && !state.ZoneExpire.IsUnknown() { + if plan.ZoneExpire.ValueInt64() != state.ZoneExpire.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить zone_expire", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneKind.IsNull() && !plan.ZoneKind.IsUnknown() && !state.ZoneKind.IsNull() && !state.ZoneKind.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ZoneKind.ValueString(), state.ZoneKind.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить zone_kind", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneMastersForSlave.IsNull() && !plan.ZoneMastersForSlave.IsUnknown() && !state.ZoneMastersForSlave.IsNull() && !state.ZoneMastersForSlave.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ZoneMastersForSlave.ValueString(), state.ZoneMastersForSlave.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить zone_masters_for_slave", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneName.IsNull() && !plan.ZoneName.IsUnknown() && !state.ZoneName.IsNull() && !state.ZoneName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ZoneName.ValueString(), state.ZoneName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить zone_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneRefresh.IsNull() && !plan.ZoneRefresh.IsUnknown() && !state.ZoneRefresh.IsNull() && !state.ZoneRefresh.IsUnknown() { + if plan.ZoneRefresh.ValueInt64() != state.ZoneRefresh.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить zone_refresh", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneRetry.IsNull() && !plan.ZoneRetry.IsUnknown() && !state.ZoneRetry.IsNull() && !state.ZoneRetry.IsUnknown() { + if plan.ZoneRetry.ValueInt64() != state.ZoneRetry.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить zone_retry", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneTTL.IsNull() && !plan.ZoneTTL.IsUnknown() && !state.ZoneTTL.IsNull() && !state.ZoneTTL.IsUnknown() { + if plan.ZoneTTL.ValueInt64() != state.ZoneTTL.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить zone_t_t_l", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.Records.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "records is required.") + return + } + if config.Records.IsUnknown() { + return + } + if config.ZoneName.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "zone_name is required.") + return + } + if config.ZoneName.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 761: resources_core.FormatString(config.ZoneName), + 762: resources_core.FormatString(config.ZoneKind), + 763: resources_core.FormatString(config.ZoneMastersForSlave), + 764: resources_core.FormatInt64(config.ZoneRefresh), + 765: resources_core.FormatInt64(config.ZoneRetry), + 766: resources_core.FormatInt64(config.ZoneExpire), + 767: resources_core.FormatInt64(config.ZoneTTL), + 768: resources_core.FormatString(config.ResourceRealm), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 109, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *ZonesV2Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data ZonesV2Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.Records.IsNull() || data.Records.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "records is required.") + return + } + if data.ZoneName.IsNull() || data.ZoneName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "zone_name is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 109, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 761: resources_core.FormatString(data.ZoneName), + 762: resources_core.FormatString(data.ZoneKind), + 763: resources_core.FormatString(data.ZoneMastersForSlave), + 764: resources_core.FormatInt64(data.ZoneRefresh), + 765: resources_core.FormatInt64(data.ZoneRetry), + 766: resources_core.FormatInt64(data.ZoneExpire), + 767: resources_core.FormatInt64(data.ZoneTTL), + 768: resources_core.FormatString(data.ResourceRealm), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 109, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 109, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "records", Field: "Records", Type: "array-map-fixed"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "zoneExpire", Field: "ZoneExpire", Type: "int64"}, + {Code: "zoneKind", Field: "ZoneKind", Type: "string"}, + {Code: "zoneMastersForSlave", Field: "ZoneMastersForSlave", Type: "string"}, + {Code: "zoneName", Field: "ZoneName", Type: "string"}, + {Code: "zoneRefresh", Field: "ZoneRefresh", Type: "int64"}, + {Code: "zoneRetry", Field: "ZoneRetry", Type: "int64"}, + {Code: "zoneTTL", Field: "ZoneTTL", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *ZonesV2Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state ZonesV2Model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 109, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "records", Field: "Records", Type: "array-map-fixed"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "zoneExpire", Field: "ZoneExpire", Type: "int64"}, + {Code: "zoneKind", Field: "ZoneKind", Type: "string"}, + {Code: "zoneMastersForSlave", Field: "ZoneMastersForSlave", Type: "string"}, + {Code: "zoneName", Field: "ZoneName", Type: "string"}, + {Code: "zoneRefresh", Field: "ZoneRefresh", Type: "int64"}, + {Code: "zoneRetry", Field: "ZoneRetry", Type: "int64"}, + {Code: "zoneTTL", Field: "ZoneTTL", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *ZonesV2Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan ZonesV2Model + var state ZonesV2Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.Records.IsNull() != state.Records.IsNull() || plan.Records.IsUnknown() != state.Records.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.Records.IsNull() && !plan.Records.IsUnknown() && !state.Records.IsNull() && !state.Records.IsUnknown() { + if !strings.EqualFold(plan.Records.ValueString(), state.Records.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 769: resources_core.FormatString(plan.Records), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 109, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "records", Field: "Records", Type: "array-map-fixed"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "zoneExpire", Field: "ZoneExpire", Type: "int64"}, + {Code: "zoneKind", Field: "ZoneKind", Type: "string"}, + {Code: "zoneMastersForSlave", Field: "ZoneMastersForSlave", Type: "string"}, + {Code: "zoneName", Field: "ZoneName", Type: "string"}, + {Code: "zoneRefresh", Field: "ZoneRefresh", Type: "int64"}, + {Code: "zoneRetry", Field: "ZoneRetry", Type: "int64"}, + {Code: "zoneTTL", Field: "ZoneTTL", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *ZonesV2Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state ZonesV2Model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *ZonesV2Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *ZonesV2Resource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/110_dnszone_resource.go b/generated/dev/go/110_dnszone_resource.go new file mode 100644 index 0000000..fab197b --- /dev/null +++ b/generated/dev/go/110_dnszone_resource.go @@ -0,0 +1,495 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: dnszone +// Service ID: 110 + +var _ resource.Resource = &DnszoneResource{} +var _ resource.ResourceWithModifyPlan = &DnszoneResource{} +var _ resource.ResourceWithImportState = &DnszoneResource{} + +type DnszoneResource struct { + client *core.UniversalClient +} + +type DnszoneModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + ResourceRealm types.String `tfsdk:"resource_realm"` + ZoneEmail types.String `tfsdk:"zone_email"` + ZoneKind types.String `tfsdk:"zone_kind"` + ZoneMastersForSlave types.String `tfsdk:"zone_masters_for_slave"` + ZoneName types.String `tfsdk:"zone_name"` + ZoneTimeToExpire types.String `tfsdk:"zone_time_to_expire"` + ZoneTimeToLive types.String `tfsdk:"zone_time_to_live"` + ZoneTimeToRefresh types.String `tfsdk:"zone_time_to_refresh"` + ZoneTimeToRetry types.String `tfsdk:"zone_time_to_retry"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewDnszoneResource() resource.Resource { + return &DnszoneResource{} +} + +func (r *DnszoneResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_dnszone" +} + +func (r *DnszoneResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("powerdns"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + "zone_email": schema.StringAttribute{Required: true, MarkdownDescription: "Контактный e-mail Не изменяется после создания."}, + "zone_kind": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("NATIVE"), MarkdownDescription: "Тип зоны Не изменяется после создания."}, + "zone_masters_for_slave": schema.StringAttribute{Optional: true, MarkdownDescription: "Использовать если выбран тип SLAVE. IP адреса в формате через пробел для настройки Master-серверов Не изменяется после создания."}, + "zone_name": schema.StringAttribute{Required: true, MarkdownDescription: "Имя зоны Не изменяется после создания."}, + "zone_time_to_expire": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("3600000"), MarkdownDescription: "Время в течение которого вторичный DNS будет пытаться завершить синхронизацию зоны с первичным Не изменяется после создания."}, + "zone_time_to_live": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("86400"), MarkdownDescription: "Время, в течение которого информация будет кешироваться другими DNS-серверами Не изменяется после создания."}, + "zone_time_to_refresh": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("86400"), MarkdownDescription: "Время ожидания ответа вторичного DNS перед запросом SOA-записи с первичных серверов Не изменяется после создания."}, + "zone_time_to_retry": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("7200"), MarkdownDescription: "Время в течение которого сервер повторно попробует синхронизировать информацию с первичных DNS-серверов Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *DnszoneResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *DnszoneModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *DnszoneModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan DnszoneModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneEmail.IsNull() && !plan.ZoneEmail.IsUnknown() && !state.ZoneEmail.IsNull() && !state.ZoneEmail.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ZoneEmail.ValueString(), state.ZoneEmail.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить zone_email", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneKind.IsNull() && !plan.ZoneKind.IsUnknown() && !state.ZoneKind.IsNull() && !state.ZoneKind.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ZoneKind.ValueString(), state.ZoneKind.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить zone_kind", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneMastersForSlave.IsNull() && !plan.ZoneMastersForSlave.IsUnknown() && !state.ZoneMastersForSlave.IsNull() && !state.ZoneMastersForSlave.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ZoneMastersForSlave.ValueString(), state.ZoneMastersForSlave.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить zone_masters_for_slave", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneName.IsNull() && !plan.ZoneName.IsUnknown() && !state.ZoneName.IsNull() && !state.ZoneName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ZoneName.ValueString(), state.ZoneName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить zone_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneTimeToExpire.IsNull() && !plan.ZoneTimeToExpire.IsUnknown() && !state.ZoneTimeToExpire.IsNull() && !state.ZoneTimeToExpire.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ZoneTimeToExpire.ValueString(), state.ZoneTimeToExpire.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить zone_time_to_expire", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneTimeToLive.IsNull() && !plan.ZoneTimeToLive.IsUnknown() && !state.ZoneTimeToLive.IsNull() && !state.ZoneTimeToLive.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ZoneTimeToLive.ValueString(), state.ZoneTimeToLive.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить zone_time_to_live", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneTimeToRefresh.IsNull() && !plan.ZoneTimeToRefresh.IsUnknown() && !state.ZoneTimeToRefresh.IsNull() && !state.ZoneTimeToRefresh.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ZoneTimeToRefresh.ValueString(), state.ZoneTimeToRefresh.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить zone_time_to_refresh", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneTimeToRetry.IsNull() && !plan.ZoneTimeToRetry.IsUnknown() && !state.ZoneTimeToRetry.IsNull() && !state.ZoneTimeToRetry.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ZoneTimeToRetry.ValueString(), state.ZoneTimeToRetry.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить zone_time_to_retry", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.ZoneEmail.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "zone_email is required.") + return + } + if config.ZoneEmail.IsUnknown() { + return + } + if config.ZoneName.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "zone_name is required.") + return + } + if config.ZoneName.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 66: resources_core.FormatString(config.ZoneEmail), + 67: resources_core.FormatString(config.ZoneName), + 68: resources_core.FormatString(config.ZoneKind), + 69: resources_core.FormatString(config.ZoneMastersForSlave), + 70: resources_core.FormatString(config.ZoneTimeToRefresh), + 71: resources_core.FormatString(config.ZoneTimeToRetry), + 72: resources_core.FormatString(config.ZoneTimeToExpire), + 73: resources_core.FormatString(config.ZoneTimeToLive), + 284: resources_core.FormatString(config.ResourceRealm), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 110, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *DnszoneResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data DnszoneModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.ZoneEmail.IsNull() || data.ZoneEmail.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "zone_email is required.") + return + } + if data.ZoneName.IsNull() || data.ZoneName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "zone_name is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 110, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 66: resources_core.FormatString(data.ZoneEmail), + 67: resources_core.FormatString(data.ZoneName), + 68: resources_core.FormatString(data.ZoneKind), + 69: resources_core.FormatString(data.ZoneMastersForSlave), + 70: resources_core.FormatString(data.ZoneTimeToRefresh), + 71: resources_core.FormatString(data.ZoneTimeToRetry), + 72: resources_core.FormatString(data.ZoneTimeToExpire), + 73: resources_core.FormatString(data.ZoneTimeToLive), + 284: resources_core.FormatString(data.ResourceRealm), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 110, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 110, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "zoneEmail", Field: "ZoneEmail", Type: "string"}, + {Code: "zoneKind", Field: "ZoneKind", Type: "string"}, + {Code: "zoneMastersForSlave", Field: "ZoneMastersForSlave", Type: "string"}, + {Code: "zoneName", Field: "ZoneName", Type: "string"}, + {Code: "zoneTimeToExpire", Field: "ZoneTimeToExpire", Type: "string"}, + {Code: "zoneTimeToLive", Field: "ZoneTimeToLive", Type: "string"}, + {Code: "zoneTimeToRefresh", Field: "ZoneTimeToRefresh", Type: "string"}, + {Code: "zoneTimeToRetry", Field: "ZoneTimeToRetry", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *DnszoneResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state DnszoneModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 110, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "zoneEmail", Field: "ZoneEmail", Type: "string"}, + {Code: "zoneKind", Field: "ZoneKind", Type: "string"}, + {Code: "zoneMastersForSlave", Field: "ZoneMastersForSlave", Type: "string"}, + {Code: "zoneName", Field: "ZoneName", Type: "string"}, + {Code: "zoneTimeToExpire", Field: "ZoneTimeToExpire", Type: "string"}, + {Code: "zoneTimeToLive", Field: "ZoneTimeToLive", Type: "string"}, + {Code: "zoneTimeToRefresh", Field: "ZoneTimeToRefresh", Type: "string"}, + {Code: "zoneTimeToRetry", Field: "ZoneTimeToRetry", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *DnszoneResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan DnszoneModel + var state DnszoneModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 110, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "zoneEmail", Field: "ZoneEmail", Type: "string"}, + {Code: "zoneKind", Field: "ZoneKind", Type: "string"}, + {Code: "zoneMastersForSlave", Field: "ZoneMastersForSlave", Type: "string"}, + {Code: "zoneName", Field: "ZoneName", Type: "string"}, + {Code: "zoneTimeToExpire", Field: "ZoneTimeToExpire", Type: "string"}, + {Code: "zoneTimeToLive", Field: "ZoneTimeToLive", Type: "string"}, + {Code: "zoneTimeToRefresh", Field: "ZoneTimeToRefresh", Type: "string"}, + {Code: "zoneTimeToRetry", Field: "ZoneTimeToRetry", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *DnszoneResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state DnszoneModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *DnszoneResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *DnszoneResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/111_dnsrecord_resource.go b/generated/dev/go/111_dnsrecord_resource.go new file mode 100644 index 0000000..b28d2d8 --- /dev/null +++ b/generated/dev/go/111_dnsrecord_resource.go @@ -0,0 +1,546 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: dnsrecord +// Service ID: 111 + +var _ resource.Resource = &DnsrecordResource{} +var _ resource.ResourceWithModifyPlan = &DnsrecordResource{} +var _ resource.ResourceWithImportState = &DnsrecordResource{} + +type DnsrecordResource struct { + client *core.UniversalClient +} + +type DnsrecordModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + RecordInput types.String `tfsdk:"record_input"` + RecordName types.String `tfsdk:"record_name"` + RecordTTL types.Int64 `tfsdk:"record_t_t_l"` + RecordType types.String `tfsdk:"record_type"` + ZoneUid types.String `tfsdk:"zone_uid"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewDnsrecordResource() resource.Resource { + return &DnsrecordResource{} +} + +func (r *DnsrecordResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_dnsrecord" +} + +func (r *DnsrecordResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "record_input": schema.StringAttribute{Required: true, MarkdownDescription: "Значение. Необходима валидация, в зависимости от типа записи. Должно быть заменено на UUID-внешнийIP"}, + "record_name": schema.StringAttribute{Required: true, MarkdownDescription: "Имя записи. Имя зоны в конце проставляется автоматически Не изменяется после создания."}, + "record_t_t_l": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(3600), MarkdownDescription: "Time-to-Leave записи. Раз в какое время запись будет обновлена Не изменяется после создания."}, + "record_type": schema.StringAttribute{Required: true, MarkdownDescription: "Тип DNS-записи Не изменяется после создания."}, + "zone_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UUID экземпляра услуги DNS-зоны Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *DnsrecordResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *DnsrecordModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *DnsrecordModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan DnsrecordModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan. + // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов. + // Resolve (displayName → UUID или uppercase → lowercase) нужен только для + // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка + // "Provider produced invalid plan: planned value does not match config value". + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.RecordName.IsNull() && !plan.RecordName.IsUnknown() && !state.RecordName.IsNull() && !state.RecordName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.RecordName.ValueString(), state.RecordName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить record_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.RecordTTL.IsNull() && !plan.RecordTTL.IsUnknown() && !state.RecordTTL.IsNull() && !state.RecordTTL.IsUnknown() { + if plan.RecordTTL.ValueInt64() != state.RecordTTL.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить record_t_t_l", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.RecordType.IsNull() && !plan.RecordType.IsUnknown() && !state.RecordType.IsNull() && !state.RecordType.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.RecordType.ValueString(), state.RecordType.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить record_type", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneUid.IsNull() && !plan.ZoneUid.IsUnknown() && !state.ZoneUid.IsNull() && !state.ZoneUid.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ZoneUid.ValueString(), state.ZoneUid.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить zone_uid", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.RecordInput.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "record_input is required.") + return + } + if config.RecordInput.IsUnknown() { + return + } + if config.RecordName.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "record_name is required.") + return + } + if config.RecordName.IsUnknown() { + return + } + if config.RecordType.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "record_type is required.") + return + } + if config.RecordType.IsUnknown() { + return + } + if config.ZoneUid.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "zone_uid is required.") + return + } + if config.ZoneUid.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + if !config.ZoneUid.IsNull() && !config.ZoneUid.IsUnknown() { + resolvedZoneUid, err := r.client.ResolveRefSvcParamValue(ctx, 109, config.ZoneUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve zone_uid", err.Error()) + } else if resolvedZoneUid != "" && resolvedZoneUid != config.ZoneUid.ValueString() { + config.ZoneUid = types.StringValue(resolvedZoneUid) + } + } + params := map[int]string{ + 75: resources_core.FormatString(config.ZoneUid), + 76: resources_core.FormatString(config.RecordType), + 77: resources_core.FormatString(config.RecordName), + 78: resources_core.FormatString(config.RecordInput), + 450: resources_core.FormatInt64(config.RecordTTL), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 111, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *DnsrecordResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data DnsrecordModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.RecordInput.IsNull() || data.RecordInput.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "record_input is required.") + return + } + if data.RecordName.IsNull() || data.RecordName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "record_name is required.") + return + } + if data.RecordType.IsNull() || data.RecordType.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "record_type is required.") + return + } + if data.ZoneUid.IsNull() || data.ZoneUid.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "zone_uid is required.") + return + } + // ══════════════════════════════════════════════════════════════════════════ + // ПРАВИЛО TERRAFORM (официальная документация): + // «If an attribute value is configured, it is NEVER valid to change that + // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что + // написал пользователь). Менять plan запрещено на уровне фреймворка. + // + // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре. + // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный). + // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит: + // «Provider produced inconsistent result after apply». + // + // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот. + // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как + // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API). + // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный + // регистр в state через strings.EqualFold (сравниваем без учёта регистра). + // ══════════════════════════════════════════════════════════════════════════ + originalZoneUid := data.ZoneUid + if !data.ZoneUid.IsNull() && !data.ZoneUid.IsUnknown() { + resolvedZoneUid, err := r.client.ResolveRefSvcParamValue(ctx, 109, data.ZoneUid.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if resolvedZoneUid != "" && resolvedZoneUid != data.ZoneUid.ValueString() { + data.ZoneUid = types.StringValue(resolvedZoneUid) + } + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 111, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 75: resources_core.FormatString(data.ZoneUid), + 76: resources_core.FormatString(data.RecordType), + 77: resources_core.FormatString(data.RecordName), + 78: resources_core.FormatString(data.RecordInput), + 450: resources_core.FormatInt64(data.RecordTTL), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 111, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 111, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "recordInput", Field: "RecordInput", Type: "string"}, + {Code: "recordName", Field: "RecordName", Type: "string"}, + {Code: "recordTTL", Field: "RecordTTL", Type: "int64"}, + {Code: "recordType", Field: "RecordType", Type: "string"}, + {Code: "zoneUid", Field: "ZoneUid", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.ZoneUid.IsNull() && !state.ZoneUid.IsUnknown() { + resolvedZoneUid, err := r.client.ResolveRefSvcParamValue(ctx, 109, state.ZoneUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve zone_uid", err.Error()) + } else if resolvedZoneUid != "" && resolvedZoneUid != state.ZoneUid.ValueString() { + state.ZoneUid = types.StringValue(resolvedZoneUid) + } + } + // Restore user-provided casing в state (Create). + // EqualFold = «совпадают ли значения без учёта регистра?» + // Если да — значит API вернул «туже» строку, только в другом регистре. + // Заменяем state на original (то что было в plan/config пользователя). + // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint. + if !originalZoneUid.IsNull() && !originalZoneUid.IsUnknown() && !state.ZoneUid.IsNull() && !state.ZoneUid.IsUnknown() { + if strings.EqualFold(state.ZoneUid.ValueString(), originalZoneUid.ValueString()) { + state.ZoneUid = originalZoneUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *DnsrecordResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state DnsrecordModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 111, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "recordInput", Field: "RecordInput", Type: "string"}, + {Code: "recordName", Field: "RecordName", Type: "string"}, + {Code: "recordTTL", Field: "RecordTTL", Type: "int64"}, + {Code: "recordType", Field: "RecordType", Type: "string"}, + {Code: "zoneUid", Field: "ZoneUid", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !newState.ZoneUid.IsNull() && !newState.ZoneUid.IsUnknown() { + resolvedZoneUid, err := r.client.ResolveRefSvcParamValue(ctx, 109, newState.ZoneUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve zone_uid", err.Error()) + } else if resolvedZoneUid != "" && resolvedZoneUid != newState.ZoneUid.ValueString() { + newState.ZoneUid = types.StringValue(resolvedZoneUid) + } + } + // Restore user-provided casing в state (Read). + // При чтении у нас нет plan — но предыдущий state уже хранит значение + // в регистре пользователя (после первого Create оно было восстановлено). + // Берём prior state (переменная state) как эталон регистра. + // Если API вернул то же UUID только строчными буквами — восстанавливаем. + if !state.ZoneUid.IsNull() && !state.ZoneUid.IsUnknown() && !newState.ZoneUid.IsNull() && !newState.ZoneUid.IsUnknown() { + if strings.EqualFold(newState.ZoneUid.ValueString(), state.ZoneUid.ValueString()) { + newState.ZoneUid = state.ZoneUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *DnsrecordResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan DnsrecordModel + var state DnsrecordModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.RecordInput.IsNull() != state.RecordInput.IsNull() || plan.RecordInput.IsUnknown() != state.RecordInput.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.RecordInput.IsNull() && !plan.RecordInput.IsUnknown() && !state.RecordInput.IsNull() && !state.RecordInput.IsUnknown() { + if !strings.EqualFold(plan.RecordInput.ValueString(), state.RecordInput.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 256: resources_core.FormatString(plan.RecordInput), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 111, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "recordInput", Field: "RecordInput", Type: "string"}, + {Code: "recordName", Field: "RecordName", Type: "string"}, + {Code: "recordTTL", Field: "RecordTTL", Type: "int64"}, + {Code: "recordType", Field: "RecordType", Type: "string"}, + {Code: "zoneUid", Field: "ZoneUid", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.ZoneUid.IsNull() && !state.ZoneUid.IsUnknown() { + resolvedZoneUid, err := r.client.ResolveRefSvcParamValue(ctx, 109, state.ZoneUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve zone_uid", err.Error()) + } else if resolvedZoneUid != "" && resolvedZoneUid != state.ZoneUid.ValueString() { + state.ZoneUid = types.StringValue(resolvedZoneUid) + } + } + // Restore user-provided casing в state (Update). + // После API-вызова modify state содержит значения в lower-case от API. + // Plan == config == то что написал пользователь (регистр неизменён). + // EqualFold: если UUID совпадает без учёта регистра — берём из plan. + if !plan.ZoneUid.IsNull() && !plan.ZoneUid.IsUnknown() && !state.ZoneUid.IsNull() && !state.ZoneUid.IsUnknown() { + if strings.EqualFold(state.ZoneUid.ValueString(), plan.ZoneUid.ValueString()) { + state.ZoneUid = plan.ZoneUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *DnsrecordResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state DnsrecordModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *DnsrecordResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *DnsrecordResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/112_tenant_resource.go b/generated/dev/go/112_tenant_resource.go new file mode 100644 index 0000000..57ca92c --- /dev/null +++ b/generated/dev/go/112_tenant_resource.go @@ -0,0 +1,372 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: tenant +// Service ID: 112 + +var _ resource.Resource = &TenantResource{} +var _ resource.ResourceWithModifyPlan = &TenantResource{} +var _ resource.ResourceWithImportState = &TenantResource{} + +type TenantResource struct { + client *core.UniversalClient +} + +type TenantModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + GrafanaOrgName types.String `tfsdk:"grafana_org_name"` + ResourceRealm types.String `tfsdk:"resource_realm"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewTenantResource() resource.Resource { + return &TenantResource{} +} + +func (r *TenantResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_tenant" +} + +func (r *TenantResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "grafana_org_name": schema.StringAttribute{Required: true, MarkdownDescription: "WZ-Код клиента Не изменяется после создания."}, + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("grafana.ngcloud.ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *TenantResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *TenantModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *TenantModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan TenantModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.GrafanaOrgName.IsNull() && !plan.GrafanaOrgName.IsUnknown() && !state.GrafanaOrgName.IsNull() && !state.GrafanaOrgName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.GrafanaOrgName.ValueString(), state.GrafanaOrgName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить grafana_org_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.GrafanaOrgName.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "grafana_org_name is required.") + return + } + if config.GrafanaOrgName.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 239: resources_core.FormatString(config.GrafanaOrgName), + 240: resources_core.FormatString(config.ResourceRealm), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 112, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *TenantResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data TenantModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.GrafanaOrgName.IsNull() || data.GrafanaOrgName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "grafana_org_name is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 112, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 239: resources_core.FormatString(data.GrafanaOrgName), + 240: resources_core.FormatString(data.ResourceRealm), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 112, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 112, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "grafanaOrgName", Field: "GrafanaOrgName", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *TenantResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state TenantModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 112, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "grafanaOrgName", Field: "GrafanaOrgName", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *TenantResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan TenantModel + var state TenantModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 112, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "grafanaOrgName", Field: "GrafanaOrgName", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *TenantResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state TenantModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *TenantResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *TenantResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/113_vc_complex_resource.go b/generated/dev/go/113_vc_complex_resource.go new file mode 100644 index 0000000..f8eace6 --- /dev/null +++ b/generated/dev/go/113_vc_complex_resource.go @@ -0,0 +1,563 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vc_complex +// Service ID: 113 + +var _ resource.Resource = &VcComplexResource{} +var _ resource.ResourceWithModifyPlan = &VcComplexResource{} +var _ resource.ResourceWithImportState = &VcComplexResource{} + +type VcComplexResource struct { + client *core.UniversalClient +} + +type VcComplexModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + CpuAllocated types.String `tfsdk:"cpu_allocated"` + CpuGuaranteed types.Int64 `tfsdk:"cpu_guaranteed"` + IpSpaceName types.String `tfsdk:"ip_space_name"` + MemAllocated types.Int64 `tfsdk:"mem_allocated"` + MemGuaranteed types.Int64 `tfsdk:"mem_guaranteed"` + NeedEnableAVI types.Bool `tfsdk:"need_enable_a_v_i"` + NeedExternalAddressSNAT types.Bool `tfsdk:"need_external_address_s_n_a_t"` + ResourceRealm types.String `tfsdk:"resource_realm"` + SegroupName types.String `tfsdk:"segroup_name"` + StorageConfig types.String `tfsdk:"storage_config"` + VIPCount types.Int64 `tfsdk:"v_i_p_count"` + VappName types.String `tfsdk:"vapp_name"` + VdcNetworkPool types.String `tfsdk:"vdc_network_pool"` + VdcProviderGateway types.String `tfsdk:"vdc_provider_gateway"` + VirtualServicesCount types.String `tfsdk:"virtual_services_count"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewVcComplexResource() resource.Resource { + return &VcComplexResource{} +} + +func (r *VcComplexResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vc_complex" +} + +func (r *VcComplexResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "cpu_allocated": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("1"), MarkdownDescription: "шт Не изменяется после создания."}, + "cpu_guaranteed": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(20), MarkdownDescription: "Не изменяется после создания."}, + "ip_space_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("internet-ipv4-v1"), MarkdownDescription: "Имя ip Space для внешнего IP. Целесообразнее брать самый дешёвый, так как это vip для исходящего трафика. Необходимо задавать если включен needExternalAddressSNAT Не изменяется после создания."}, + "mem_allocated": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "gb Не изменяется после создания."}, + "mem_guaranteed": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(20), MarkdownDescription: "Не изменяется после создания."}, + "need_enable_a_v_i": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включает AVI в Cloud Director Не изменяется после создания."}, + "need_external_address_s_n_a_t": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Выделяет внешний IP на Edge. Используется для SNAT всего трафика Не изменяется после создания."}, + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("sandbox.nubes.ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + "segroup_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("SEGROUP-SANDBOX-CL1-SHARED-01"), MarkdownDescription: "Наименование segroup, в котором будут выделяться пулы на AVI Не изменяется после создания."}, + "storage_config": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{\"name\": \"\", \"size\": 1024}]"), MarkdownDescription: "Описывается как массив объектов, где описывается имя политики и её размер Не изменяется после создания.", PlanModifiers: []planmodifier.String{resources_core.JsonNormalize()}}, + "v_i_p_count": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(0), MarkdownDescription: "шт Не изменяется после создания."}, + "vapp_name": schema.StringAttribute{Required: true, MarkdownDescription: "Имя vApp Не изменяется после создания."}, + "vdc_network_pool": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("nsxt-sandbox-geneve-np"), MarkdownDescription: "Не изменяется после создания."}, + "vdc_provider_gateway": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("sandbox-v1cl1-pvdc"), MarkdownDescription: "Взять из свойств ресурсной платформы.
Путь .vcd.providerVdc - взять KEY

providerGateway Cloud Director. Влияет ресурсы будут предоставлены. Является полем (List<String>) в свойствах рес. платформы ЛК, но сам параметр передаётся только как один String Не изменяется после создания."}, + "virtual_services_count": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("1"), MarkdownDescription: "Кол-во виртуальных сервисов, которые выделяются на AVI. Во избежании коллапса пока выделяется до 4 Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VcComplexResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *VcComplexModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *VcComplexModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan VcComplexModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.CpuAllocated.IsNull() && !plan.CpuAllocated.IsUnknown() && !state.CpuAllocated.IsNull() && !state.CpuAllocated.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.CpuAllocated.ValueString(), state.CpuAllocated.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить cpu_allocated", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.CpuGuaranteed.IsNull() && !plan.CpuGuaranteed.IsUnknown() && !state.CpuGuaranteed.IsNull() && !state.CpuGuaranteed.IsUnknown() { + if plan.CpuGuaranteed.ValueInt64() != state.CpuGuaranteed.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить cpu_guaranteed", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.IpSpaceName.IsNull() && !plan.IpSpaceName.IsUnknown() && !state.IpSpaceName.IsNull() && !state.IpSpaceName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.IpSpaceName.ValueString(), state.IpSpaceName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить ip_space_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.MemAllocated.IsNull() && !plan.MemAllocated.IsUnknown() && !state.MemAllocated.IsNull() && !state.MemAllocated.IsUnknown() { + if plan.MemAllocated.ValueInt64() != state.MemAllocated.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить mem_allocated", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.MemGuaranteed.IsNull() && !plan.MemGuaranteed.IsUnknown() && !state.MemGuaranteed.IsNull() && !state.MemGuaranteed.IsUnknown() { + if plan.MemGuaranteed.ValueInt64() != state.MemGuaranteed.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить mem_guaranteed", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.NeedEnableAVI.IsNull() && !plan.NeedEnableAVI.IsUnknown() && !state.NeedEnableAVI.IsNull() && !state.NeedEnableAVI.IsUnknown() { + if plan.NeedEnableAVI.ValueBool() != state.NeedEnableAVI.ValueBool() { + resp.Diagnostics.AddError("Нельзя изменить need_enable_a_v_i", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.NeedExternalAddressSNAT.IsNull() && !plan.NeedExternalAddressSNAT.IsUnknown() && !state.NeedExternalAddressSNAT.IsNull() && !state.NeedExternalAddressSNAT.IsUnknown() { + if plan.NeedExternalAddressSNAT.ValueBool() != state.NeedExternalAddressSNAT.ValueBool() { + resp.Diagnostics.AddError("Нельзя изменить need_external_address_s_n_a_t", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.SegroupName.IsNull() && !plan.SegroupName.IsUnknown() && !state.SegroupName.IsNull() && !state.SegroupName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.SegroupName.ValueString(), state.SegroupName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить segroup_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.StorageConfig.IsNull() && !plan.StorageConfig.IsUnknown() && !state.StorageConfig.IsNull() && !state.StorageConfig.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.StorageConfig.ValueString(), state.StorageConfig.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить storage_config", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.VIPCount.IsNull() && !plan.VIPCount.IsUnknown() && !state.VIPCount.IsNull() && !state.VIPCount.IsUnknown() { + if plan.VIPCount.ValueInt64() != state.VIPCount.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить v_i_p_count", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.VappName.IsNull() && !plan.VappName.IsUnknown() && !state.VappName.IsNull() && !state.VappName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VappName.ValueString(), state.VappName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить vapp_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.VdcNetworkPool.IsNull() && !plan.VdcNetworkPool.IsUnknown() && !state.VdcNetworkPool.IsNull() && !state.VdcNetworkPool.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VdcNetworkPool.ValueString(), state.VdcNetworkPool.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить vdc_network_pool", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.VdcProviderGateway.IsNull() && !plan.VdcProviderGateway.IsUnknown() && !state.VdcProviderGateway.IsNull() && !state.VdcProviderGateway.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VdcProviderGateway.ValueString(), state.VdcProviderGateway.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить vdc_provider_gateway", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.VirtualServicesCount.IsNull() && !plan.VirtualServicesCount.IsUnknown() && !state.VirtualServicesCount.IsNull() && !state.VirtualServicesCount.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VirtualServicesCount.ValueString(), state.VirtualServicesCount.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить virtual_services_count", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.VappName.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "vapp_name is required.") + return + } + if config.VappName.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 332: resources_core.FormatString(config.ResourceRealm), + 334: resources_core.FormatString(config.VappName), + 358: resources_core.FormatString(config.VdcProviderGateway), + 359: resources_core.FormatString(config.VirtualServicesCount), + 360: resources_core.FormatString(config.IpSpaceName), + 399: resources_core.FormatString(config.StorageConfig), + 400: resources_core.FormatString(config.VdcNetworkPool), + 401: resources_core.FormatInt64(config.CpuGuaranteed), + 402: resources_core.FormatInt64(config.MemGuaranteed), + 403: resources_core.FormatString(config.SegroupName), + 404: resources_core.FormatBool(config.NeedExternalAddressSNAT), + 405: resources_core.FormatBool(config.NeedEnableAVI), + 566: resources_core.FormatString(config.CpuAllocated), + 567: resources_core.FormatInt64(config.MemAllocated), + 568: resources_core.FormatInt64(config.VIPCount), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 113, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *VcComplexResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data VcComplexModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.VappName.IsNull() || data.VappName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "vapp_name is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 113, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 332: resources_core.FormatString(data.ResourceRealm), + 334: resources_core.FormatString(data.VappName), + 358: resources_core.FormatString(data.VdcProviderGateway), + 359: resources_core.FormatString(data.VirtualServicesCount), + 360: resources_core.FormatString(data.IpSpaceName), + 399: resources_core.FormatString(data.StorageConfig), + 400: resources_core.FormatString(data.VdcNetworkPool), + 401: resources_core.FormatInt64(data.CpuGuaranteed), + 402: resources_core.FormatInt64(data.MemGuaranteed), + 403: resources_core.FormatString(data.SegroupName), + 404: resources_core.FormatBool(data.NeedExternalAddressSNAT), + 405: resources_core.FormatBool(data.NeedEnableAVI), + 566: resources_core.FormatString(data.CpuAllocated), + 567: resources_core.FormatInt64(data.MemAllocated), + 568: resources_core.FormatInt64(data.VIPCount), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 113, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 113, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "cpuAllocated", Field: "CpuAllocated", Type: "string"}, + {Code: "cpuGuaranteed", Field: "CpuGuaranteed", Type: "int64"}, + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "memAllocated", Field: "MemAllocated", Type: "int64"}, + {Code: "memGuaranteed", Field: "MemGuaranteed", Type: "int64"}, + {Code: "needEnableAVI", Field: "NeedEnableAVI", Type: "bool"}, + {Code: "needExternalAddressSNAT", Field: "NeedExternalAddressSNAT", Type: "bool"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "segroupName", Field: "SegroupName", Type: "string"}, + {Code: "storageConfig", Field: "StorageConfig", Type: "string"}, + {Code: "vIPCount", Field: "VIPCount", Type: "int64"}, + {Code: "vappName", Field: "VappName", Type: "string"}, + {Code: "vdcNetworkPool", Field: "VdcNetworkPool", Type: "string"}, + {Code: "vdcProviderGateway", Field: "VdcProviderGateway", Type: "string"}, + {Code: "virtualServicesCount", Field: "VirtualServicesCount", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcComplexResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VcComplexModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 113, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "cpuAllocated", Field: "CpuAllocated", Type: "string"}, + {Code: "cpuGuaranteed", Field: "CpuGuaranteed", Type: "int64"}, + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "memAllocated", Field: "MemAllocated", Type: "int64"}, + {Code: "memGuaranteed", Field: "MemGuaranteed", Type: "int64"}, + {Code: "needEnableAVI", Field: "NeedEnableAVI", Type: "bool"}, + {Code: "needExternalAddressSNAT", Field: "NeedExternalAddressSNAT", Type: "bool"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "segroupName", Field: "SegroupName", Type: "string"}, + {Code: "storageConfig", Field: "StorageConfig", Type: "string"}, + {Code: "vIPCount", Field: "VIPCount", Type: "int64"}, + {Code: "vappName", Field: "VappName", Type: "string"}, + {Code: "vdcNetworkPool", Field: "VdcNetworkPool", Type: "string"}, + {Code: "vdcProviderGateway", Field: "VdcProviderGateway", Type: "string"}, + {Code: "virtualServicesCount", Field: "VirtualServicesCount", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *VcComplexResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VcComplexModel + var state VcComplexModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 113, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "cpuAllocated", Field: "CpuAllocated", Type: "string"}, + {Code: "cpuGuaranteed", Field: "CpuGuaranteed", Type: "int64"}, + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "memAllocated", Field: "MemAllocated", Type: "int64"}, + {Code: "memGuaranteed", Field: "MemGuaranteed", Type: "int64"}, + {Code: "needEnableAVI", Field: "NeedEnableAVI", Type: "bool"}, + {Code: "needExternalAddressSNAT", Field: "NeedExternalAddressSNAT", Type: "bool"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "segroupName", Field: "SegroupName", Type: "string"}, + {Code: "storageConfig", Field: "StorageConfig", Type: "string"}, + {Code: "vIPCount", Field: "VIPCount", Type: "int64"}, + {Code: "vappName", Field: "VappName", Type: "string"}, + {Code: "vdcNetworkPool", Field: "VdcNetworkPool", Type: "string"}, + {Code: "vdcProviderGateway", Field: "VdcProviderGateway", Type: "string"}, + {Code: "virtualServicesCount", Field: "VirtualServicesCount", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcComplexResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VcComplexModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *VcComplexResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *VcComplexResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/115_mariadb_database_resource.go b/generated/dev/go/115_mariadb_database_resource.go new file mode 100644 index 0000000..cd6dd52 --- /dev/null +++ b/generated/dev/go/115_mariadb_database_resource.go @@ -0,0 +1,343 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: mariadb +// Subresource: database + +var _ resource.Resource = &MariadbDatabaseResource{} + +// MariadbDatabaseResource manages a subresource via operations. +type MariadbDatabaseResource struct { + client *core.UniversalClient +} + +type MariadbDatabaseModel struct { + ID types.String `tfsdk:"id"` + MariadbID types.String `tfsdk:"mariadb_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + DbName types.String `tfsdk:"db_name"` +} + +func NewMariadbDatabaseResource() resource.Resource { + return &MariadbDatabaseResource{} +} + +func (r *MariadbDatabaseResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_mariadb_database" +} + +func (r *MariadbDatabaseResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "mariadb_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "db_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("db-dxinlvxx"), MarkdownDescription: "Пример: `db01` Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *MariadbDatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan MariadbDatabaseModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.MariadbID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "dbName": resources_core.FormatString(plan.DbName), + } + identityCodes := []string{ + "dbName", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("database", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "dbName": resources_core.FormatString(plan.DbName), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_database", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_database", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *MariadbDatabaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state MariadbDatabaseModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *MariadbDatabaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan MariadbDatabaseModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state MariadbDatabaseModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.MariadbID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{ + "dbName": resources_core.FormatString(plan.DbName), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *MariadbDatabaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state MariadbDatabaseModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_database" == "" { + return + } + + instanceUID := strings.TrimSpace(state.MariadbID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "dbName": resources_core.FormatString(state.DbName), + } + identityCodes := []string{ + "dbName", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("database", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "dbName": resources_core.FormatString(state.DbName), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_database", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *MariadbDatabaseResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/115_mariadb_resource.go b/generated/dev/go/115_mariadb_resource.go new file mode 100644 index 0000000..8ed7b1d --- /dev/null +++ b/generated/dev/go/115_mariadb_resource.go @@ -0,0 +1,447 @@ +package resources_gen + +import ( + "context" + "fmt" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: mariadb +// Service ID: 115 + +var _ resource.Resource = &MariadbResource{} +var _ resource.ResourceWithModifyPlan = &MariadbResource{} +var _ resource.ResourceWithImportState = &MariadbResource{} + +type MariadbResource struct { + client *core.UniversalClient +} + +// MariadbAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration. +type MariadbAccessConfigurationModel struct { + MasterIpSpace types.String `tfsdk:"master_ip_space" json:"masterIpSpace"` + MasterAccessList types.String `tfsdk:"master_access_list" json:"masterAccessList"` +} + +// MariadbAutoscaleConfigurationModel — вложенная модель для map-fixed параметра autoscaleConfiguration. +type MariadbAutoscaleConfigurationModel struct { + Enabled types.Bool `tfsdk:"enabled" json:"enabled"` + Schedule types.Int64 `tfsdk:"schedule" json:"schedule"` + Quota types.Int64 `tfsdk:"quota" json:"quota"` + Percent types.Int64 `tfsdk:"percent" json:"percent"` +} + +// MariadbBackupConfigurationModel — вложенная модель для map-fixed параметра backupConfiguration. +type MariadbBackupConfigurationModel struct { + S3Uid types.String `tfsdk:"s3_uid" json:"s3Uid"` + Retain types.Int64 `tfsdk:"retain" json:"retain"` + Schedule types.String `tfsdk:"schedule" json:"schedule"` +} + +// MariadbClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type MariadbClusterConfigurationModel struct { + Memory types.Int64 `tfsdk:"memory" json:"memory"` + Replicas types.Int64 `tfsdk:"replicas" json:"replicas"` + Disk types.Int64 `tfsdk:"disk" json:"disk"` + Cpu types.Int64 `tfsdk:"cpu" json:"cpu"` +} + +// MariadbMariadbConfigurationModel — вложенная модель для map-fixed параметра mariadbConfiguration. +type MariadbMariadbConfigurationModel struct { + Version types.String `tfsdk:"version" json:"version"` +} + +// MariadbStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type MariadbStartupConfigurationModel struct { + ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"` +} + +type MariadbModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessConfiguration *MariadbAccessConfigurationModel `tfsdk:"access_configuration"` + AutoscaleConfiguration *MariadbAutoscaleConfigurationModel `tfsdk:"autoscale_configuration"` + BackupConfiguration *MariadbBackupConfigurationModel `tfsdk:"backup_configuration"` + ClusterConfiguration *MariadbClusterConfigurationModel `tfsdk:"cluster_configuration"` + MariadbConfiguration *MariadbMariadbConfigurationModel `tfsdk:"mariadb_configuration"` + StartupConfiguration *MariadbStartupConfigurationModel `tfsdk:"startup_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewMariadbResource() resource.Resource { + return &MariadbResource{} +} + +func (r *MariadbResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_mariadb" +} + +func (r *MariadbResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "master_ip_space": schema.StringAttribute{Required: true, MarkdownDescription: "Из какого Ip-Space резервировать внешний IP
Список предоставляется из Организации, в котором развёрнута ресурсная платформа"}, + "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"}, + }}, + "autoscale_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "enabled": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включает автоскейлинг PV. При включении параметра необходимо настроить autoScalePercentage и autoScaleTechWindow. Алерт при срабатывании вызывает modify у текущей услуги"}, + "schedule": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(0), MarkdownDescription: "Требует включения autoScale. Окно обновления (пока не реализовано). Указывается час в виде Integer (0 / 5 / 12 / 23)"}, + "quota": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100), MarkdownDescription: "До какого размера возможно увеличивать диск, когда включен Autoscale"}, + "percent": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Требует включения autoScale. Позволяет указать в процентах расширение от текущего макс объема
Минимальный размер на который расширяется - **1Gb**"}, + }}, + "backup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "s3_uid": schema.StringAttribute{Required: true, MarkdownDescription: "Экземпляр S3 обязателен"}, + "retain": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(720)}, + "schedule": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("0 0 * * *")}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Указывается в Megabytes"}, + "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Пока `Galera` выключена
Как будет запрос на включение и тестирование, реплики можно будет увеличивать"}, + "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Указывается в GIGAbytes"}, + "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"}, + }}, + "mariadb_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("9.4.0")}, + }}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"}, + }}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *MariadbResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *MariadbModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *MariadbModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan MariadbModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 877: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}), + 878: resources_core.BuildJSON(map[string]string{"memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64())}), + 879: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterAccessList.ValueString())}), + 880: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.MariadbConfiguration.Version.ValueString())}), + 881: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", config.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", config.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", config.BackupConfiguration.Schedule.ValueString())}), + 882: resources_core.BuildJSON(map[string]string{"enabled": fmt.Sprintf("%v", config.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", config.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", config.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", config.AutoscaleConfiguration.Percent.ValueInt64())}), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 115, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *MariadbResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data MariadbModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 115, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 877: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}), + 878: resources_core.BuildJSON(map[string]string{"memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64())}), + 879: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterAccessList.ValueString())}), + 880: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.MariadbConfiguration.Version.ValueString())}), + 881: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", data.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", data.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", data.BackupConfiguration.Schedule.ValueString())}), + 882: resources_core.BuildJSON(map[string]string{"enabled": fmt.Sprintf("%v", data.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", data.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", data.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", data.AutoscaleConfiguration.Percent.ValueInt64())}), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 115, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 115, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"}, + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "mariadbConfiguration", Field: "MariadbConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *MariadbResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state MariadbModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 115, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"}, + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "mariadbConfiguration", Field: "MariadbConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *MariadbResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan MariadbModel + var state MariadbModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 883: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64())}), + 884: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterAccessList.ValueString())}), + 885: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", plan.MariadbConfiguration.Version.ValueString())}), + 886: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", plan.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", plan.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", plan.BackupConfiguration.Schedule.ValueString())}), + 887: resources_core.BuildJSON(map[string]string{"enabled": fmt.Sprintf("%v", plan.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Percent.ValueInt64())}), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 115, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"}, + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "mariadbConfiguration", Field: "MariadbConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *MariadbResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state MariadbModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *MariadbResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *MariadbResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/115_mariadb_user_resource.go b/generated/dev/go/115_mariadb_user_resource.go new file mode 100644 index 0000000..5a2f790 --- /dev/null +++ b/generated/dev/go/115_mariadb_user_resource.go @@ -0,0 +1,367 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: mariadb +// Subresource: user + +var _ resource.Resource = &MariadbUserResource{} + +// MariadbUserResource manages a subresource via operations. +type MariadbUserResource struct { + client *core.UniversalClient +} + +type MariadbUserModel struct { + ID types.String `tfsdk:"id"` + MariadbID types.String `tfsdk:"mariadb_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessHosts types.String `tfsdk:"access_hosts"` + DbName types.String `tfsdk:"db_name"` + Password types.String `tfsdk:"password"` + Username types.String `tfsdk:"username"` +} + +func NewMariadbUserResource() resource.Resource { + return &MariadbUserResource{} +} + +func (r *MariadbUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_mariadb_user" +} + +func (r *MariadbUserResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "mariadb_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_hosts": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("%"), MarkdownDescription: "Пример: `%` Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "db_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Необходимо указать массив существующих баз Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "password": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("mhs3keq0"), MarkdownDescription: "Секрет будет записан в хранилище секретов и не будет отображаться публично Не изменяется после создания. Изменение требует пересоздания.", Sensitive: true, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "username": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("userz2hsgoyo"), MarkdownDescription: "Пример: `myusername` Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *MariadbUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan MariadbUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.MariadbID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "accessHosts": resources_core.FormatString(plan.AccessHosts), + "dbName": resources_core.FormatString(plan.DbName), + "password": resources_core.FormatString(plan.Password), + "username": resources_core.FormatString(plan.Username), + } + identityCodes := []string{ + "accessHosts", + "dbName", + "password", + "username", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "username": resources_core.FormatString(plan.Username), + "password": resources_core.FormatString(plan.Password), + "dbName": resources_core.FormatString(plan.DbName), + "accessHosts": resources_core.FormatString(plan.AccessHosts), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *MariadbUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state MariadbUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *MariadbUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan MariadbUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state MariadbUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.MariadbID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{ + "accessHosts": resources_core.FormatString(plan.AccessHosts), + "dbName": resources_core.FormatString(plan.DbName), + "password": resources_core.FormatString(plan.Password), + "username": resources_core.FormatString(plan.Username), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *MariadbUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state MariadbUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_user" == "" { + return + } + + instanceUID := strings.TrimSpace(state.MariadbID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "accessHosts": resources_core.FormatString(state.AccessHosts), + "dbName": resources_core.FormatString(state.DbName), + "password": resources_core.FormatString(state.Password), + "username": resources_core.FormatString(state.Username), + } + identityCodes := []string{ + "accessHosts", + "dbName", + "password", + "username", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "username": resources_core.FormatString(state.Username), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_user", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *MariadbUserResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/116_kafka_resource.go b/generated/dev/go/116_kafka_resource.go new file mode 100644 index 0000000..402172e --- /dev/null +++ b/generated/dev/go/116_kafka_resource.go @@ -0,0 +1,389 @@ +package resources_gen + +import ( + "context" + "fmt" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: kafka +// Service ID: 116 + +var _ resource.Resource = &KafkaResource{} +var _ resource.ResourceWithModifyPlan = &KafkaResource{} +var _ resource.ResourceWithImportState = &KafkaResource{} + +type KafkaResource struct { + client *core.UniversalClient +} + +// KafkaAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration. +type KafkaAccessConfigurationModel struct { + AllocationExternal types.Bool `tfsdk:"allocation_external" json:"allocationExternal"` + MasterAccessList types.String `tfsdk:"master_access_list" json:"masterAccessList"` +} + +// KafkaClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type KafkaClusterConfigurationModel struct { + Cpu types.Int64 `tfsdk:"cpu" json:"cpu"` + Memory types.Int64 `tfsdk:"memory" json:"memory"` + Replicas types.Int64 `tfsdk:"replicas" json:"replicas"` + Disk types.Int64 `tfsdk:"disk" json:"disk"` +} + +// KafkaStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type KafkaStartupConfigurationModel struct { + ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"` +} + +type KafkaModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessConfiguration *KafkaAccessConfigurationModel `tfsdk:"access_configuration"` + ClusterConfiguration *KafkaClusterConfigurationModel `tfsdk:"cluster_configuration"` + StartupConfiguration *KafkaStartupConfigurationModel `tfsdk:"startup_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewKafkaResource() resource.Resource { + return &KafkaResource{} +} + +func (r *KafkaResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_kafka" +} + +func (r *KafkaResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "allocation_external": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2000), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"}, + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2048)}, + "replicas": schema.Int64Attribute{Required: true}, + "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)}, + }}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"}, + }}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *KafkaResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *KafkaModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *KafkaModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan KafkaModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 1084: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}), + 1085: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64())}), + 1086: resources_core.BuildJSON(map[string]string{"allocationExternal": fmt.Sprintf("%v", config.AccessConfiguration.AllocationExternal.ValueBool()), "masterAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterAccessList.ValueString())}), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 116, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *KafkaResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data KafkaModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 116, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 1084: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}), + 1085: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64())}), + 1086: resources_core.BuildJSON(map[string]string{"allocationExternal": fmt.Sprintf("%v", data.AccessConfiguration.AllocationExternal.ValueBool()), "masterAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterAccessList.ValueString())}), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 116, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 116, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *KafkaResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state KafkaModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 116, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *KafkaResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan KafkaModel + var state KafkaModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 1087: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64())}), + 1088: resources_core.BuildJSON(map[string]string{"allocationExternal": fmt.Sprintf("%v", plan.AccessConfiguration.AllocationExternal.ValueBool()), "masterAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterAccessList.ValueString())}), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 116, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *KafkaResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state KafkaModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *KafkaResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *KafkaResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/116_kafka_topic_resource.go b/generated/dev/go/116_kafka_topic_resource.go new file mode 100644 index 0000000..3f289b3 --- /dev/null +++ b/generated/dev/go/116_kafka_topic_resource.go @@ -0,0 +1,364 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: kafka +// Subresource: topic + +var _ resource.Resource = &KafkaTopicResource{} + +// KafkaTopicResource manages a subresource via operations. +type KafkaTopicResource struct { + client *core.UniversalClient +} + +type KafkaTopicModel struct { + ID types.String `tfsdk:"id"` + KafkaID types.String `tfsdk:"kafka_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + NameTopic types.String `tfsdk:"name_topic"` + Partitions types.Int64 `tfsdk:"partitions"` + Replicas types.Int64 `tfsdk:"replicas"` +} + +func NewKafkaTopicResource() resource.Resource { + return &KafkaTopicResource{} +} + +func (r *KafkaTopicResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_kafka_topic" +} + +func (r *KafkaTopicResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "kafka_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "name_topic": schema.StringAttribute{Required: true, MarkdownDescription: "Пример: `orders-events` Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "partitions": schema.Int64Attribute{Required: true, MarkdownDescription: "Число партиций можно увеличить, но уменьшить их нельзя."}, + "replicas": schema.Int64Attribute{Required: true, MarkdownDescription: "Количество реплик не может привышать количество реплик брокера"}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *KafkaTopicResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan KafkaTopicModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if plan.NameTopic.IsNull() || plan.NameTopic.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "name_topic is required.") + return + } + if plan.Partitions.IsNull() || plan.Partitions.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "partitions is required.") + return + } + if plan.Replicas.IsNull() || plan.Replicas.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "replicas is required.") + return + } + + instanceUID := strings.TrimSpace(plan.KafkaID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "nameTopic": resources_core.FormatString(plan.NameTopic), + } + identityCodes := []string{ + "nameTopic", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("topic", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "topic", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "nameTopic": resources_core.FormatString(plan.NameTopic), + "partitions": resources_core.FormatInt64(plan.Partitions), + "replicas": resources_core.FormatInt64(plan.Replicas), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_topic", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_topic", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "topic", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "topic", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "topic", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *KafkaTopicResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state KafkaTopicModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *KafkaTopicResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan KafkaTopicModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "modify_topic" == "" { + var state KafkaTopicModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.KafkaID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{ + "nameTopic": resources_core.FormatString(plan.NameTopic), + "partitions": resources_core.FormatInt64(plan.Partitions), + "replicas": resources_core.FormatInt64(plan.Replicas), + }) + + idParams := map[string]string{ + "nameTopic": resources_core.FormatString(plan.NameTopic), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "modify_topic", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "topic", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *KafkaTopicResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state KafkaTopicModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_topic" == "" { + return + } + + instanceUID := strings.TrimSpace(state.KafkaID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "nameTopic": resources_core.FormatString(state.NameTopic), + } + identityCodes := []string{ + "nameTopic", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("topic", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "nameTopic": resources_core.FormatString(state.NameTopic), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_topic", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *KafkaTopicResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/116_kafka_user_resource.go b/generated/dev/go/116_kafka_user_resource.go new file mode 100644 index 0000000..52ea73c --- /dev/null +++ b/generated/dev/go/116_kafka_user_resource.go @@ -0,0 +1,373 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: kafka +// Subresource: user + +var _ resource.Resource = &KafkaUserResource{} + +// KafkaUserResource manages a subresource via operations. +type KafkaUserResource struct { + client *core.UniversalClient +} + +type KafkaUserModel struct { + ID types.String `tfsdk:"id"` + KafkaID types.String `tfsdk:"kafka_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessHosts types.String `tfsdk:"access_hosts"` + Group types.String `tfsdk:"group"` + NameTopic types.String `tfsdk:"name_topic"` + Operations types.String `tfsdk:"operations"` + Username types.String `tfsdk:"username"` +} + +func NewKafkaUserResource() resource.Resource { + return &KafkaUserResource{} +} + +func (r *KafkaUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_kafka_user" +} + +func (r *KafkaUserResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "kafka_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_hosts": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("*"), MarkdownDescription: "Пример: `192.168.1.0/24` или `*`"}, + "group": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("*"), MarkdownDescription: "Пример: `order-consumers` или `*`"}, + "name_topic": schema.StringAttribute{Required: true, MarkdownDescription: "Пример: `orders-events`. Использовать маску нельзя(`orders*`), только полное название или `*`"}, + "operations": schema.StringAttribute{Required: true, MarkdownDescription: "Пример: `Create, Describe, Read, Write`"}, + "username": schema.StringAttribute{Required: true, MarkdownDescription: "Пример: `username-admin` Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *KafkaUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan KafkaUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if plan.NameTopic.IsNull() || plan.NameTopic.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "name_topic is required.") + return + } + if plan.Operations.IsNull() || plan.Operations.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "operations is required.") + return + } + if plan.Username.IsNull() || plan.Username.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "username is required.") + return + } + + instanceUID := strings.TrimSpace(plan.KafkaID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "username": resources_core.FormatString(plan.Username), + } + identityCodes := []string{ + "username", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "username": resources_core.FormatString(plan.Username), + "nameTopic": resources_core.FormatString(plan.NameTopic), + "operations": resources_core.FormatString(plan.Operations), + "group": resources_core.FormatString(plan.Group), + "accessHosts": resources_core.FormatString(plan.AccessHosts), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *KafkaUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state KafkaUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *KafkaUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan KafkaUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "modify_user" == "" { + var state KafkaUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.KafkaID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{ + "nameTopic": resources_core.FormatString(plan.NameTopic), + "operations": resources_core.FormatString(plan.Operations), + "group": resources_core.FormatString(plan.Group), + "accessHosts": resources_core.FormatString(plan.AccessHosts), + "username": resources_core.FormatString(plan.Username), + }) + + idParams := map[string]string{ + "username": resources_core.FormatString(plan.Username), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "modify_user", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *KafkaUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state KafkaUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_user" == "" { + return + } + + instanceUID := strings.TrimSpace(state.KafkaID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "username": resources_core.FormatString(state.Username), + } + identityCodes := []string{ + "username", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "username": resources_core.FormatString(state.Username), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_user", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *KafkaUserResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/119_akhq_resource.go b/generated/dev/go/119_akhq_resource.go new file mode 100644 index 0000000..101c30a --- /dev/null +++ b/generated/dev/go/119_akhq_resource.go @@ -0,0 +1,554 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: akhq +// Service ID: 119 + +var _ resource.Resource = &AkhqResource{} +var _ resource.ResourceWithModifyPlan = &AkhqResource{} +var _ resource.ResourceWithImportState = &AkhqResource{} + +type AkhqResource struct { + client *core.UniversalClient +} + +type AkhqModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + Domain types.String `tfsdk:"domain"` + KafkaUid types.String `tfsdk:"kafka_uid"` + ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"` + ResourceInstances types.Int64 `tfsdk:"resource_instances"` + ResourceMemory types.Int64 `tfsdk:"resource_memory"` + ResourceRealm types.String `tfsdk:"resource_realm"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewAkhqResource() resource.Resource { + return &AkhqResource{} +} + +func (r *AkhqResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_akhq" +} + +func (r *AkhqResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "domain": schema.StringAttribute{Required: true, MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."}, + "kafka_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "Выбрать кластер из списка"}, + "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Пример: `1000` Не изменяется после создания."}, + "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Не изменяется после создания."}, + "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Пример: `1024` Не изменяется после создания."}, + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Выбрать платформу из списка Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *AkhqResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *AkhqModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *AkhqModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan AkhqModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan. + // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов. + // Resolve (displayName → UUID или uppercase → lowercase) нужен только для + // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка + // "Provider produced invalid plan: planned value does not match config value". + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() { + if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_c_p_u", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() { + if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_instances", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() { + if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_memory", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.Domain.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "domain is required.") + return + } + if config.Domain.IsUnknown() { + return + } + if config.KafkaUid.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "kafka_uid is required.") + return + } + if config.KafkaUid.IsUnknown() { + return + } + if config.ResourceRealm.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + return + } + if config.ResourceRealm.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + if !config.KafkaUid.IsNull() && !config.KafkaUid.IsUnknown() { + resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, config.KafkaUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve kafka_uid", err.Error()) + } else if resolvedKafkaUid != "" && resolvedKafkaUid != config.KafkaUid.ValueString() { + config.KafkaUid = types.StringValue(resolvedKafkaUid) + } + } + params := map[int]string{ + 550: resources_core.FormatString(config.ResourceRealm), + 551: resources_core.FormatInt64(config.ResourceMemory), + 552: resources_core.FormatInt64(config.ResourceCPU), + 553: resources_core.FormatString(config.Domain), + 554: resources_core.FormatInt64(config.ResourceInstances), + 555: resources_core.FormatString(config.KafkaUid), + } + desiredDomain := "" + if !config.Domain.IsNull() && !config.Domain.IsUnknown() { + desiredDomain = config.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 119, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *AkhqResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data AkhqModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.Domain.IsNull() || data.Domain.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "domain is required.") + return + } + if data.KafkaUid.IsNull() || data.KafkaUid.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "kafka_uid is required.") + return + } + if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + return + } + // ══════════════════════════════════════════════════════════════════════════ + // ПРАВИЛО TERRAFORM (официальная документация): + // «If an attribute value is configured, it is NEVER valid to change that + // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что + // написал пользователь). Менять plan запрещено на уровне фреймворка. + // + // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре. + // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный). + // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит: + // «Provider produced inconsistent result after apply». + // + // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот. + // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как + // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API). + // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный + // регистр в state через strings.EqualFold (сравниваем без учёта регистра). + // ══════════════════════════════════════════════════════════════════════════ + originalKafkaUid := data.KafkaUid + if !data.KafkaUid.IsNull() && !data.KafkaUid.IsUnknown() { + resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, data.KafkaUid.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if resolvedKafkaUid != "" && resolvedKafkaUid != data.KafkaUid.ValueString() { + data.KafkaUid = types.StringValue(resolvedKafkaUid) + } + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + if !data.Domain.IsNull() && !data.Domain.IsUnknown() { + desiredDomain = data.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 119, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 550: resources_core.FormatString(data.ResourceRealm), + 551: resources_core.FormatInt64(data.ResourceMemory), + 552: resources_core.FormatInt64(data.ResourceCPU), + 553: resources_core.FormatString(data.Domain), + 554: resources_core.FormatInt64(data.ResourceInstances), + 555: resources_core.FormatString(data.KafkaUid), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 119, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 119, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "kafkaUid", Field: "KafkaUid", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() { + resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, state.KafkaUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve kafka_uid", err.Error()) + } else if resolvedKafkaUid != "" && resolvedKafkaUid != state.KafkaUid.ValueString() { + state.KafkaUid = types.StringValue(resolvedKafkaUid) + } + } + // Restore user-provided casing в state (Create). + // EqualFold = «совпадают ли значения без учёта регистра?» + // Если да — значит API вернул «туже» строку, только в другом регистре. + // Заменяем state на original (то что было в plan/config пользователя). + // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint. + if !originalKafkaUid.IsNull() && !originalKafkaUid.IsUnknown() && !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() { + if strings.EqualFold(state.KafkaUid.ValueString(), originalKafkaUid.ValueString()) { + state.KafkaUid = originalKafkaUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *AkhqResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state AkhqModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 119, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "kafkaUid", Field: "KafkaUid", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !newState.KafkaUid.IsNull() && !newState.KafkaUid.IsUnknown() { + resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, newState.KafkaUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve kafka_uid", err.Error()) + } else if resolvedKafkaUid != "" && resolvedKafkaUid != newState.KafkaUid.ValueString() { + newState.KafkaUid = types.StringValue(resolvedKafkaUid) + } + } + // Restore user-provided casing в state (Read). + // При чтении у нас нет plan — но предыдущий state уже хранит значение + // в регистре пользователя (после первого Create оно было восстановлено). + // Берём prior state (переменная state) как эталон регистра. + // Если API вернул то же UUID только строчными буквами — восстанавливаем. + if !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() && !newState.KafkaUid.IsNull() && !newState.KafkaUid.IsUnknown() { + if strings.EqualFold(newState.KafkaUid.ValueString(), state.KafkaUid.ValueString()) { + newState.KafkaUid = state.KafkaUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *AkhqResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan AkhqModel + var state AkhqModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.KafkaUid.IsNull() != state.KafkaUid.IsNull() || plan.KafkaUid.IsUnknown() != state.KafkaUid.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.KafkaUid.IsNull() && !plan.KafkaUid.IsUnknown() && !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() { + if !strings.EqualFold(plan.KafkaUid.ValueString(), state.KafkaUid.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 565: resources_core.FormatString(plan.KafkaUid), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 119, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "kafkaUid", Field: "KafkaUid", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() { + resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, state.KafkaUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve kafka_uid", err.Error()) + } else if resolvedKafkaUid != "" && resolvedKafkaUid != state.KafkaUid.ValueString() { + state.KafkaUid = types.StringValue(resolvedKafkaUid) + } + } + // Restore user-provided casing в state (Update). + // После API-вызова modify state содержит значения в lower-case от API. + // Plan == config == то что написал пользователь (регистр неизменён). + // EqualFold: если UUID совпадает без учёта регистра — берём из plan. + if !plan.KafkaUid.IsNull() && !plan.KafkaUid.IsUnknown() && !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() { + if strings.EqualFold(state.KafkaUid.ValueString(), plan.KafkaUid.ValueString()) { + state.KafkaUid = plan.KafkaUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *AkhqResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state AkhqModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *AkhqResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *AkhqResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/120_clickhouse_database_resource.go b/generated/dev/go/120_clickhouse_database_resource.go new file mode 100644 index 0000000..cc5c42e --- /dev/null +++ b/generated/dev/go/120_clickhouse_database_resource.go @@ -0,0 +1,343 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: clickhouse +// Subresource: database + +var _ resource.Resource = &ClickhouseDatabaseResource{} + +// ClickhouseDatabaseResource manages a subresource via operations. +type ClickhouseDatabaseResource struct { + client *core.UniversalClient +} + +type ClickhouseDatabaseModel struct { + ID types.String `tfsdk:"id"` + ClickhouseID types.String `tfsdk:"clickhouse_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + DbName types.String `tfsdk:"db_name"` +} + +func NewClickhouseDatabaseResource() resource.Resource { + return &ClickhouseDatabaseResource{} +} + +func (r *ClickhouseDatabaseResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_clickhouse_database" +} + +func (r *ClickhouseDatabaseResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "clickhouse_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "db_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("db01"), MarkdownDescription: "Пример: `db01` Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *ClickhouseDatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan ClickhouseDatabaseModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.ClickhouseID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "dbName": resources_core.FormatString(plan.DbName), + } + identityCodes := []string{ + "dbName", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("database", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "dbName": resources_core.FormatString(plan.DbName), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_database", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_database", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *ClickhouseDatabaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state ClickhouseDatabaseModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *ClickhouseDatabaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan ClickhouseDatabaseModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state ClickhouseDatabaseModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.ClickhouseID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{ + "dbName": resources_core.FormatString(plan.DbName), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *ClickhouseDatabaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state ClickhouseDatabaseModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_database" == "" { + return + } + + instanceUID := strings.TrimSpace(state.ClickhouseID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "dbName": resources_core.FormatString(state.DbName), + } + identityCodes := []string{ + "dbName", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("database", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "dbName": resources_core.FormatString(state.DbName), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_database", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *ClickhouseDatabaseResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/120_clickhouse_resource.go b/generated/dev/go/120_clickhouse_resource.go new file mode 100644 index 0000000..12affc2 --- /dev/null +++ b/generated/dev/go/120_clickhouse_resource.go @@ -0,0 +1,487 @@ +package resources_gen + +import ( + "context" + "fmt" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: clickhouse +// Service ID: 120 + +var _ resource.Resource = &ClickhouseResource{} +var _ resource.ResourceWithModifyPlan = &ClickhouseResource{} +var _ resource.ResourceWithImportState = &ClickhouseResource{} + +type ClickhouseResource struct { + client *core.UniversalClient +} + +// ClickhouseAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration. +type ClickhouseAccessConfigurationModel struct { + MasterIpSpace types.String `tfsdk:"master_ip_space" json:"masterIpSpace"` + MasterAccessList types.String `tfsdk:"master_access_list" json:"masterAccessList"` +} + +// ClickhouseAutoscaleConfigurationModel — вложенная модель для map-fixed параметра autoscaleConfiguration. +type ClickhouseAutoscaleConfigurationModel struct { + Enabled types.Bool `tfsdk:"enabled" json:"enabled"` + Schedule types.Int64 `tfsdk:"schedule" json:"schedule"` + Quota types.Int64 `tfsdk:"quota" json:"quota"` + Percent types.Int64 `tfsdk:"percent" json:"percent"` +} + +// ClickhouseBackupConfigurationModel — вложенная модель для map-fixed параметра backupConfiguration. +type ClickhouseBackupConfigurationModel struct { + S3Uid types.String `tfsdk:"s3_uid" json:"s3Uid"` + Retain types.Int64 `tfsdk:"retain" json:"retain"` + Schedule types.String `tfsdk:"schedule" json:"schedule"` +} + +// ClickhouseClickhouseConfigurationModel — вложенная модель для map-fixed параметра clickhouseConfiguration. +type ClickhouseClickhouseConfigurationModel struct { + Version types.String `tfsdk:"version" json:"version"` +} + +// ClickhouseClickhouseKeeperConfigurationModel — вложенная модель для map-fixed параметра clickhouseKeeperConfiguration. +type ClickhouseClickhouseKeeperConfigurationModel struct { + Cpu types.Int64 `tfsdk:"cpu" json:"cpu"` + Memory types.Int64 `tfsdk:"memory" json:"memory"` + Replicas types.Int64 `tfsdk:"replicas" json:"replicas"` +} + +// ClickhouseClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type ClickhouseClusterConfigurationModel struct { + Memory types.Int64 `tfsdk:"memory" json:"memory"` + Replicas types.Int64 `tfsdk:"replicas" json:"replicas"` + Shards types.Int64 `tfsdk:"shards" json:"shards"` + Disk types.Int64 `tfsdk:"disk" json:"disk"` + Cpu types.Int64 `tfsdk:"cpu" json:"cpu"` +} + +// ClickhouseStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type ClickhouseStartupConfigurationModel struct { + ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"` +} + +type ClickhouseModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessConfiguration *ClickhouseAccessConfigurationModel `tfsdk:"access_configuration"` + AutoscaleConfiguration *ClickhouseAutoscaleConfigurationModel `tfsdk:"autoscale_configuration"` + BackupConfiguration *ClickhouseBackupConfigurationModel `tfsdk:"backup_configuration"` + ClickhouseConfiguration *ClickhouseClickhouseConfigurationModel `tfsdk:"clickhouse_configuration"` + ClickhouseFilesConfiguration types.String `tfsdk:"clickhouse_files_configuration"` + ClickhouseKeeperConfiguration *ClickhouseClickhouseKeeperConfigurationModel `tfsdk:"clickhouse_keeper_configuration"` + ClusterConfiguration *ClickhouseClusterConfigurationModel `tfsdk:"cluster_configuration"` + StartupConfiguration *ClickhouseStartupConfigurationModel `tfsdk:"startup_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewClickhouseResource() resource.Resource { + return &ClickhouseResource{} +} + +func (r *ClickhouseResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_clickhouse" +} + +func (r *ClickhouseResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "master_ip_space": schema.StringAttribute{Required: true, MarkdownDescription: "Из какого Ip-Space резервировать внешний IP
Список предоставляется из Организации, в котором развёрнута ресурсная платформа"}, + "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"}, + }}, + "autoscale_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "enabled": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включает автоскейлинг PV. При включении параметра необходимо настроить autoScalePercentage и autoScaleTechWindow. Алерт при срабатывании вызывает modify у текущей услуги"}, + "schedule": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(0), MarkdownDescription: "Требует включения autoScale. Окно обновления (пока не реализовано). Указывается час в виде Integer (0 / 5 / 12 / 23)"}, + "quota": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100), MarkdownDescription: "До какого размера возможно увеличивать диск, когда включен Autoscale"}, + "percent": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Требует включения autoScale. Позволяет указать в процентах расширение от текущего макс объема
Минимальный размер на который расширяется - **1Gb**"}, + }}, + "backup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "s3_uid": schema.StringAttribute{Required: true, MarkdownDescription: "Экземпляр S3 обязателен"}, + "retain": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(7)}, + "schedule": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("0 0 * * *")}, + }}, + "clickhouse_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("25.10")}, + }}, + "clickhouse_files_configuration": schema.StringAttribute{Optional: true, MarkdownDescription: "Формат:
- path: somethings3.xml
xmlConfig: '<clickhouse>......</clickhouse>'"}, + "clickhouse_keeper_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"}, + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Указывается в Megabytes"}, + "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(0), MarkdownDescription: "Если указан 0, то инстансы ClickHouse Keeper развёрнуты не будут"}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2048), MarkdownDescription: "Указывается в Megabytes"}, + "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)}, + "shards": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "**Шард** - это часть данных, разделенная на разные узлы (серверы) для горизонтального масштабирования и обработки больших объемов информации.
**Реплика** - это точная копия данных внутри одного шарда"}, + "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Указывается в GIGAbytes"}, + "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1000), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"}, + }}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"}, + }}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *ClickhouseResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *ClickhouseModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *ClickhouseModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan ClickhouseModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 837: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}), + 838: resources_core.BuildJSON(map[string]string{"memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "shards": fmt.Sprintf("%d", config.ClusterConfiguration.Shards.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64())}), + 839: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterAccessList.ValueString())}), + 840: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClickhouseKeeperConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClickhouseKeeperConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClickhouseKeeperConfiguration.Replicas.ValueInt64())}), + 841: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.ClickhouseConfiguration.Version.ValueString())}), + 842: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", config.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", config.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", config.BackupConfiguration.Schedule.ValueString())}), + 843: resources_core.BuildJSON(map[string]string{"enabled": fmt.Sprintf("%v", config.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", config.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", config.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", config.AutoscaleConfiguration.Percent.ValueInt64())}), + 851: resources_core.FormatString(config.ClickhouseFilesConfiguration), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 120, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *ClickhouseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data ClickhouseModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 120, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 837: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}), + 838: resources_core.BuildJSON(map[string]string{"memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "shards": fmt.Sprintf("%d", data.ClusterConfiguration.Shards.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64())}), + 839: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterAccessList.ValueString())}), + 840: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClickhouseKeeperConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClickhouseKeeperConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClickhouseKeeperConfiguration.Replicas.ValueInt64())}), + 841: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.ClickhouseConfiguration.Version.ValueString())}), + 842: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", data.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", data.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", data.BackupConfiguration.Schedule.ValueString())}), + 843: resources_core.BuildJSON(map[string]string{"enabled": fmt.Sprintf("%v", data.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", data.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", data.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", data.AutoscaleConfiguration.Percent.ValueInt64())}), + 851: resources_core.FormatString(data.ClickhouseFilesConfiguration), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 120, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 120, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"}, + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"}, + {Code: "clickhouseConfiguration", Field: "ClickhouseConfiguration", Type: "map-fixed"}, + {Code: "clickhouseFilesConfiguration", Field: "ClickhouseFilesConfiguration", Type: "array-map-fixed"}, + {Code: "clickhouseKeeperConfiguration", Field: "ClickhouseKeeperConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *ClickhouseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state ClickhouseModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 120, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"}, + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"}, + {Code: "clickhouseConfiguration", Field: "ClickhouseConfiguration", Type: "map-fixed"}, + {Code: "clickhouseFilesConfiguration", Field: "ClickhouseFilesConfiguration", Type: "array-map-fixed"}, + {Code: "clickhouseKeeperConfiguration", Field: "ClickhouseKeeperConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *ClickhouseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan ClickhouseModel + var state ClickhouseModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.ClickhouseFilesConfiguration.IsNull() != state.ClickhouseFilesConfiguration.IsNull() || plan.ClickhouseFilesConfiguration.IsUnknown() != state.ClickhouseFilesConfiguration.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ClickhouseFilesConfiguration.IsNull() && !plan.ClickhouseFilesConfiguration.IsUnknown() && !state.ClickhouseFilesConfiguration.IsNull() && !state.ClickhouseFilesConfiguration.IsUnknown() { + if !strings.EqualFold(plan.ClickhouseFilesConfiguration.ValueString(), state.ClickhouseFilesConfiguration.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 844: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "shards": fmt.Sprintf("%d", plan.ClusterConfiguration.Shards.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64())}), + 845: resources_core.BuildJSON(map[string]string{"replicas": fmt.Sprintf("%d", plan.ClickhouseKeeperConfiguration.Replicas.ValueInt64()), "cpu": fmt.Sprintf("%d", plan.ClickhouseKeeperConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClickhouseKeeperConfiguration.Memory.ValueInt64())}), + 846: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterAccessList.ValueString())}), + 847: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", plan.ClickhouseConfiguration.Version.ValueString())}), + 848: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", plan.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", plan.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", plan.BackupConfiguration.Schedule.ValueString())}), + 849: resources_core.BuildJSON(map[string]string{"enabled": fmt.Sprintf("%v", plan.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Percent.ValueInt64())}), + 850: resources_core.FormatString(plan.ClickhouseFilesConfiguration), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 120, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"}, + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"}, + {Code: "clickhouseConfiguration", Field: "ClickhouseConfiguration", Type: "map-fixed"}, + {Code: "clickhouseFilesConfiguration", Field: "ClickhouseFilesConfiguration", Type: "array-map-fixed"}, + {Code: "clickhouseKeeperConfiguration", Field: "ClickhouseKeeperConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *ClickhouseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state ClickhouseModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *ClickhouseResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *ClickhouseResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/120_clickhouse_user_resource.go b/generated/dev/go/120_clickhouse_user_resource.go new file mode 100644 index 0000000..8dc28f7 --- /dev/null +++ b/generated/dev/go/120_clickhouse_user_resource.go @@ -0,0 +1,359 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: clickhouse +// Subresource: user + +var _ resource.Resource = &ClickhouseUserResource{} + +// ClickhouseUserResource manages a subresource via operations. +type ClickhouseUserResource struct { + client *core.UniversalClient +} + +type ClickhouseUserModel struct { + ID types.String `tfsdk:"id"` + ClickhouseID types.String `tfsdk:"clickhouse_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessHosts types.String `tfsdk:"access_hosts"` + DbName types.String `tfsdk:"db_name"` + Username types.String `tfsdk:"username"` +} + +func NewClickhouseUserResource() resource.Resource { + return &ClickhouseUserResource{} +} + +func (r *ClickhouseUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_clickhouse_user" +} + +func (r *ClickhouseUserResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "clickhouse_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_hosts": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("::/0"), MarkdownDescription: "Пример: `::/0` Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "db_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "username": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("myusername"), MarkdownDescription: "Пример: `myusername` Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *ClickhouseUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan ClickhouseUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.ClickhouseID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "accessHosts": resources_core.FormatString(plan.AccessHosts), + "dbName": resources_core.FormatString(plan.DbName), + "username": resources_core.FormatString(plan.Username), + } + identityCodes := []string{ + "accessHosts", + "dbName", + "username", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "username": resources_core.FormatString(plan.Username), + "dbName": resources_core.FormatString(plan.DbName), + "accessHosts": resources_core.FormatString(plan.AccessHosts), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *ClickhouseUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state ClickhouseUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *ClickhouseUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan ClickhouseUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state ClickhouseUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.ClickhouseID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{ + "accessHosts": resources_core.FormatString(plan.AccessHosts), + "dbName": resources_core.FormatString(plan.DbName), + "username": resources_core.FormatString(plan.Username), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *ClickhouseUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state ClickhouseUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_user" == "" { + return + } + + instanceUID := strings.TrimSpace(state.ClickhouseID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "accessHosts": resources_core.FormatString(state.AccessHosts), + "dbName": resources_core.FormatString(state.DbName), + "username": resources_core.FormatString(state.Username), + } + identityCodes := []string{ + "accessHosts", + "dbName", + "username", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "username": resources_core.FormatString(state.Username), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_user", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *ClickhouseUserResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/12_s3_resource.go b/generated/dev/go/12_s3_resource.go new file mode 100644 index 0000000..f4db5ce --- /dev/null +++ b/generated/dev/go/12_s3_resource.go @@ -0,0 +1,421 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: s3 +// Service ID: 12 + +var _ resource.Resource = &S3Resource{} +var _ resource.ResourceWithModifyPlan = &S3Resource{} +var _ resource.ResourceWithImportState = &S3Resource{} + +type S3Resource struct { + client *core.UniversalClient +} + +type S3Model struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + DisplayName types.String `tfsdk:"display_name"` + MaxBucketsPerUser types.Int64 `tfsdk:"max_buckets_per_user"` + MaxObjectsPerBucket types.Int64 `tfsdk:"max_objects_per_bucket"` + MaxSizeGbPerUser types.Int64 `tfsdk:"max_size_gb_per_user"` + ResourceRealm types.String `tfsdk:"resource_realm"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewS3Resource() resource.Resource { + return &S3Resource{} +} + +func (r *S3Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_s3" +} + +func (r *S3Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "display_name": schema.StringAttribute{Required: true, MarkdownDescription: "Человеко-читаемое имя (транслит из Elma) - передаётся имя клиента Не изменяется после создания."}, + "max_buckets_per_user": schema.Int64Attribute{Optional: true, MarkdownDescription: "Квота на пользователя. Количество бакетов пользователя. Передаём руками"}, + "max_objects_per_bucket": schema.Int64Attribute{Optional: true, MarkdownDescription: "Квота на бакет. Кол-во объектов. Передаём руками"}, + "max_size_gb_per_user": schema.Int64Attribute{Optional: true, MarkdownDescription: "maxSizeGb. Передаём руками"}, + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("ceph.tst.nubes.ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *S3Resource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *S3Model + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *S3Model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan S3Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.DisplayName.IsNull() && !plan.DisplayName.IsUnknown() && !state.DisplayName.IsNull() && !state.DisplayName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.DisplayName.ValueString(), state.DisplayName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить display_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.DisplayName.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "display_name is required.") + return + } + if config.DisplayName.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 163: resources_core.FormatString(config.ResourceRealm), + 320: resources_core.FormatString(config.DisplayName), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 12, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *S3Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data S3Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.DisplayName.IsNull() || data.DisplayName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "display_name is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 12, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 163: resources_core.FormatString(data.ResourceRealm), + 320: resources_core.FormatString(data.DisplayName), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 12, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 12, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "displayName", Field: "DisplayName", Type: "string"}, + {Code: "maxBucketsPerUser", Field: "MaxBucketsPerUser", Type: "int64"}, + {Code: "maxObjectsPerBucket", Field: "MaxObjectsPerBucket", Type: "int64"}, + {Code: "maxSizeGbPerUser", Field: "MaxSizeGbPerUser", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *S3Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state S3Model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 12, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "displayName", Field: "DisplayName", Type: "string"}, + {Code: "maxBucketsPerUser", Field: "MaxBucketsPerUser", Type: "int64"}, + {Code: "maxObjectsPerBucket", Field: "MaxObjectsPerBucket", Type: "int64"}, + {Code: "maxSizeGbPerUser", Field: "MaxSizeGbPerUser", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *S3Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan S3Model + var state S3Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.MaxSizeGbPerUser.IsNull() != state.MaxSizeGbPerUser.IsNull() || plan.MaxSizeGbPerUser.IsUnknown() != state.MaxSizeGbPerUser.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.MaxSizeGbPerUser.IsNull() && !plan.MaxSizeGbPerUser.IsUnknown() && !state.MaxSizeGbPerUser.IsNull() && !state.MaxSizeGbPerUser.IsUnknown() { + if plan.MaxSizeGbPerUser.ValueInt64() != state.MaxSizeGbPerUser.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.MaxObjectsPerBucket.IsNull() != state.MaxObjectsPerBucket.IsNull() || plan.MaxObjectsPerBucket.IsUnknown() != state.MaxObjectsPerBucket.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.MaxObjectsPerBucket.IsNull() && !plan.MaxObjectsPerBucket.IsUnknown() && !state.MaxObjectsPerBucket.IsNull() && !state.MaxObjectsPerBucket.IsUnknown() { + if plan.MaxObjectsPerBucket.ValueInt64() != state.MaxObjectsPerBucket.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.MaxBucketsPerUser.IsNull() != state.MaxBucketsPerUser.IsNull() || plan.MaxBucketsPerUser.IsUnknown() != state.MaxBucketsPerUser.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.MaxBucketsPerUser.IsNull() && !plan.MaxBucketsPerUser.IsUnknown() && !state.MaxBucketsPerUser.IsNull() && !state.MaxBucketsPerUser.IsUnknown() { + if plan.MaxBucketsPerUser.ValueInt64() != state.MaxBucketsPerUser.ValueInt64() { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 50: resources_core.FormatInt64(plan.MaxSizeGbPerUser), + 51: resources_core.FormatInt64(plan.MaxObjectsPerBucket), + 52: resources_core.FormatInt64(plan.MaxBucketsPerUser), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 12, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "displayName", Field: "DisplayName", Type: "string"}, + {Code: "maxBucketsPerUser", Field: "MaxBucketsPerUser", Type: "int64"}, + {Code: "maxObjectsPerBucket", Field: "MaxObjectsPerBucket", Type: "int64"}, + {Code: "maxSizeGbPerUser", Field: "MaxSizeGbPerUser", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *S3Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state S3Model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *S3Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *S3Resource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/12_s3_sub_user_resource.go b/generated/dev/go/12_s3_sub_user_resource.go new file mode 100644 index 0000000..b8e0168 --- /dev/null +++ b/generated/dev/go/12_s3_sub_user_resource.go @@ -0,0 +1,358 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: s3 +// Subresource: sub_user + +var _ resource.Resource = &S3SubUserResource{} + +// S3SubUserResource manages a subresource via operations. +type S3SubUserResource struct { + client *core.UniversalClient +} + +type S3SubUserModel struct { + ID types.String `tfsdk:"id"` + S3ID types.String `tfsdk:"s3_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + SubUserAccess types.String `tfsdk:"sub_user_access"` + SubUserName types.String `tfsdk:"sub_user_name"` +} + +func NewS3SubUserResource() resource.Resource { + return &S3SubUserResource{} +} + +func (r *S3SubUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_s3_sub_user" +} + +func (r *S3SubUserResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "s3_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "sub_user_access": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "sub_user_name": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *S3SubUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan S3SubUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if plan.SubUserAccess.IsNull() || plan.SubUserAccess.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "sub_user_access is required.") + return + } + if plan.SubUserName.IsNull() || plan.SubUserName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "sub_user_name is required.") + return + } + + instanceUID := strings.TrimSpace(plan.S3ID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "subUserAccess": resources_core.FormatString(plan.SubUserAccess), + "subUserName": resources_core.FormatString(plan.SubUserName), + } + identityCodes := []string{ + "subUserAccess", + "subUserName", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("sub_user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "sub_user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "subUserName": resources_core.FormatString(plan.SubUserName), + "subUserAccess": resources_core.FormatString(plan.SubUserAccess), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_sub_user", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_sub_user", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "sub_user", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "sub_user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "sub_user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *S3SubUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state S3SubUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *S3SubUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan S3SubUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state S3SubUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.S3ID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{ + "subUserAccess": resources_core.FormatString(plan.SubUserAccess), + "subUserName": resources_core.FormatString(plan.SubUserName), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "sub_user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *S3SubUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state S3SubUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_sub_user" == "" { + return + } + + instanceUID := strings.TrimSpace(state.S3ID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "subUserAccess": resources_core.FormatString(state.SubUserAccess), + "subUserName": resources_core.FormatString(state.SubUserName), + } + identityCodes := []string{ + "subUserAccess", + "subUserName", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("sub_user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "subUserName": resources_core.FormatString(state.SubUserName), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_sub_user", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *S3SubUserResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/13_s3bucket_resource.go b/generated/dev/go/13_s3bucket_resource.go new file mode 100644 index 0000000..deb2272 --- /dev/null +++ b/generated/dev/go/13_s3bucket_resource.go @@ -0,0 +1,478 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: s3bucket +// Service ID: 13 + +var _ resource.Resource = &S3bucketResource{} +var _ resource.ResourceWithModifyPlan = &S3bucketResource{} +var _ resource.ResourceWithImportState = &S3bucketResource{} + +type S3bucketResource struct { + client *core.UniversalClient +} + +type S3bucketModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + BucketName types.String `tfsdk:"bucket_name"` + CorsAll types.Bool `tfsdk:"cors_all"` + ListAll types.Bool `tfsdk:"list_all"` + MaxSize types.String `tfsdk:"max_size"` + Placement types.String `tfsdk:"placement"` + ReadAll types.Bool `tfsdk:"read_all"` + S3UserUid types.String `tfsdk:"s3_user_uid"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewS3bucketResource() resource.Resource { + return &S3bucketResource{} +} + +func (r *S3bucketResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_s3bucket" +} + +func (r *S3bucketResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "bucket_name": schema.StringAttribute{Required: true, MarkdownDescription: "Имя бакета s3 Не изменяется после создания."}, + "cors_all": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Публичный доступ к чтению настроек позволяет любому пользователю интернета читать конфигурации CORS Не изменяется после создания."}, + "list_all": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Публичный доступ к листингу позволяет любому пользователю интернета получить список объектов в бакете. Не изменяется после создания."}, + "max_size": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("-1"), MarkdownDescription: "Максимальный размер бакета s3. Если поставить -1, будет безлимит. Размер указывается в Bytes Не изменяется после создания."}, + "placement": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("HOT"), MarkdownDescription: "Класс хранилища, который используется по умолчанию при загрузке объектов. Чем холоднее, тем дешевле место, но дороже обращения Не изменяется после создания."}, + "read_all": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Публичный доступ на чтение позволяет любому пользователю интернета скачивать объекты из бакета. Не изменяется после создания."}, + "s3_user_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UID Корневой услуги S3. На основе него будет создан бакет. Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *S3bucketResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *S3bucketModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *S3bucketModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan S3bucketModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan. + // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов. + // Resolve (displayName → UUID или uppercase → lowercase) нужен только для + // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка + // "Provider produced invalid plan: planned value does not match config value". + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.BucketName.IsNull() && !plan.BucketName.IsUnknown() && !state.BucketName.IsNull() && !state.BucketName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.BucketName.ValueString(), state.BucketName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить bucket_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.CorsAll.IsNull() && !plan.CorsAll.IsUnknown() && !state.CorsAll.IsNull() && !state.CorsAll.IsUnknown() { + if plan.CorsAll.ValueBool() != state.CorsAll.ValueBool() { + resp.Diagnostics.AddError("Нельзя изменить cors_all", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ListAll.IsNull() && !plan.ListAll.IsUnknown() && !state.ListAll.IsNull() && !state.ListAll.IsUnknown() { + if plan.ListAll.ValueBool() != state.ListAll.ValueBool() { + resp.Diagnostics.AddError("Нельзя изменить list_all", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.MaxSize.IsNull() && !plan.MaxSize.IsUnknown() && !state.MaxSize.IsNull() && !state.MaxSize.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.MaxSize.ValueString(), state.MaxSize.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить max_size", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.Placement.IsNull() && !plan.Placement.IsUnknown() && !state.Placement.IsNull() && !state.Placement.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Placement.ValueString(), state.Placement.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить placement", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ReadAll.IsNull() && !plan.ReadAll.IsUnknown() && !state.ReadAll.IsNull() && !state.ReadAll.IsUnknown() { + if plan.ReadAll.ValueBool() != state.ReadAll.ValueBool() { + resp.Diagnostics.AddError("Нельзя изменить read_all", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.S3UserUid.IsNull() && !plan.S3UserUid.IsUnknown() && !state.S3UserUid.IsNull() && !state.S3UserUid.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.S3UserUid.ValueString(), state.S3UserUid.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить s3_user_uid", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.BucketName.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "bucket_name is required.") + return + } + if config.BucketName.IsUnknown() { + return + } + if config.S3UserUid.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "s3_user_uid is required.") + return + } + if config.S3UserUid.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + if !config.S3UserUid.IsNull() && !config.S3UserUid.IsUnknown() { + resolvedS3UserUid, err := r.client.ResolveRefSvcParamValue(ctx, 12, config.S3UserUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve s3_user_uid", err.Error()) + } else if resolvedS3UserUid != "" && resolvedS3UserUid != config.S3UserUid.ValueString() { + config.S3UserUid = types.StringValue(resolvedS3UserUid) + } + } + params := map[int]string{ + 124: resources_core.FormatString(config.S3UserUid), + 125: resources_core.FormatString(config.BucketName), + 126: resources_core.FormatString(config.MaxSize), + 127: resources_core.FormatBool(config.ReadAll), + 128: resources_core.FormatBool(config.ListAll), + 129: resources_core.FormatBool(config.CorsAll), + 130: resources_core.FormatString(config.Placement), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 13, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *S3bucketResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data S3bucketModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.BucketName.IsNull() || data.BucketName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "bucket_name is required.") + return + } + if data.S3UserUid.IsNull() || data.S3UserUid.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "s3_user_uid is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 13, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 124: resources_core.FormatString(data.S3UserUid), + 125: resources_core.FormatString(data.BucketName), + 126: resources_core.FormatString(data.MaxSize), + 127: resources_core.FormatBool(data.ReadAll), + 128: resources_core.FormatBool(data.ListAll), + 129: resources_core.FormatBool(data.CorsAll), + 130: resources_core.FormatString(data.Placement), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 13, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 13, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "bucketName", Field: "BucketName", Type: "string"}, + {Code: "corsAll", Field: "CorsAll", Type: "bool"}, + {Code: "listAll", Field: "ListAll", Type: "bool"}, + {Code: "maxSize", Field: "MaxSize", Type: "string"}, + {Code: "placement", Field: "Placement", Type: "string"}, + {Code: "readAll", Field: "ReadAll", Type: "bool"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + // Restore user-provided casing в state (Create). + // EqualFold = «совпадают ли значения без учёта регистра?» + // Если да — значит API вернул «туже» строку, только в другом регистре. + // Заменяем state на original (то что было в plan/config пользователя). + // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint. + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *S3bucketResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state S3bucketModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 13, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "bucketName", Field: "BucketName", Type: "string"}, + {Code: "corsAll", Field: "CorsAll", Type: "bool"}, + {Code: "listAll", Field: "ListAll", Type: "bool"}, + {Code: "maxSize", Field: "MaxSize", Type: "string"}, + {Code: "placement", Field: "Placement", Type: "string"}, + {Code: "readAll", Field: "ReadAll", Type: "bool"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + // Restore user-provided casing в state (Read). + // При чтении у нас нет plan — но предыдущий state уже хранит значение + // в регистре пользователя (после первого Create оно было восстановлено). + // Берём prior state (переменная state) как эталон регистра. + // Если API вернул то же UUID только строчными буквами — восстанавливаем. + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *S3bucketResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan S3bucketModel + var state S3bucketModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 13, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "bucketName", Field: "BucketName", Type: "string"}, + {Code: "corsAll", Field: "CorsAll", Type: "bool"}, + {Code: "listAll", Field: "ListAll", Type: "bool"}, + {Code: "maxSize", Field: "MaxSize", Type: "string"}, + {Code: "placement", Field: "Placement", Type: "string"}, + {Code: "readAll", Field: "ReadAll", Type: "bool"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + // Restore user-provided casing в state (Update). + // После API-вызова modify state содержит значения в lower-case от API. + // Plan == config == то что написал пользователь (регистр неизменён). + // EqualFold: если UUID совпадает без учёта регистра — берём из plan. + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *S3bucketResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state S3bucketModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *S3bucketResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *S3bucketResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/148_vc_mgmt_sthutrval_cluster_resource.go b/generated/dev/go/148_vc_mgmt_sthutrval_cluster_resource.go new file mode 100644 index 0000000..17540bd --- /dev/null +++ b/generated/dev/go/148_vc_mgmt_sthutrval_cluster_resource.go @@ -0,0 +1,419 @@ +package resources_gen + +import ( + "context" + "fmt" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vc_mgmt_sthutrval_cluster +// Service ID: 148 + +var _ resource.Resource = &VcMgmtSthutrvalClusterResource{} +var _ resource.ResourceWithModifyPlan = &VcMgmtSthutrvalClusterResource{} +var _ resource.ResourceWithImportState = &VcMgmtSthutrvalClusterResource{} + +type VcMgmtSthutrvalClusterResource struct { + client *core.UniversalClient +} + +// VcMgmtSthutrvalClusterAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration. +type VcMgmtSthutrvalClusterAccessConfigurationModel struct { + AccessIpListAPI types.String `tfsdk:"access_ip_list_a_p_i" json:"accessIpListAPI"` + NeedExternalAddressIngress types.Bool `tfsdk:"need_external_address_ingress" json:"needExternalAddressIngress"` + AccessIpListIngress types.String `tfsdk:"access_ip_list_ingress" json:"accessIpListIngress"` + NeedExternalAddressAPI types.Bool `tfsdk:"need_external_address_a_p_i" json:"needExternalAddressAPI"` +} + +// VcMgmtSthutrvalClusterClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type VcMgmtSthutrvalClusterClusterConfigurationModel struct { + AppVersion types.String `tfsdk:"app_version" json:"appVersion"` +} + +// VcMgmtSthutrvalClusterControlPlaneConfigurationModel — вложенная модель для map-fixed параметра controlPlaneConfiguration. +type VcMgmtSthutrvalClusterControlPlaneConfigurationModel struct { + SizingDisk types.Int64 `tfsdk:"sizing_disk" json:"sizingDisk"` + Count types.Int64 `tfsdk:"count" json:"count"` + SizingPolicy types.String `tfsdk:"sizing_policy" json:"sizingPolicy"` +} + +// VcMgmtSthutrvalClusterStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type VcMgmtSthutrvalClusterStartupConfigurationModel struct { + ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"` + ClusterName types.String `tfsdk:"cluster_name" json:"clusterName"` +} + +type VcMgmtSthutrvalClusterModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessConfiguration *VcMgmtSthutrvalClusterAccessConfigurationModel `tfsdk:"access_configuration"` + ClusterConfiguration *VcMgmtSthutrvalClusterClusterConfigurationModel `tfsdk:"cluster_configuration"` + ControlPlaneConfiguration *VcMgmtSthutrvalClusterControlPlaneConfigurationModel `tfsdk:"control_plane_configuration"` + StartupConfiguration *VcMgmtSthutrvalClusterStartupConfigurationModel `tfsdk:"startup_configuration"` + WorkerConfiguration types.String `tfsdk:"worker_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewVcMgmtSthutrvalClusterResource() resource.Resource { + return &VcMgmtSthutrvalClusterResource{} +} + +func (r *VcMgmtSthutrvalClusterResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vc_mgmt_sthutrval_cluster" +} + +func (r *VcMgmtSthutrvalClusterResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "access_ip_list_a_p_i": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Access IP List для подключения к Kubernetes API. Указывается массив. Если передан пустой массив, доступ выделяется всем
Пример: **["0.0.0.0/0"]** или **[]** или **["178.4.13.2", "178.4.14.0/24"]**"}, + "need_external_address_ingress": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Выделяет внешний IP для доступа к Ingress"}, + "access_ip_list_ingress": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Access IP List для подключения к Ingress. Указывается массив. Если передан пустой массив, доступ выделяется всем
Пример: **["0.0.0.0/0"]** или **[]** или **["178.4.13.2", "178.4.14.0/24"]**"}, + "need_external_address_a_p_i": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Выделяет внешний IP для доступа к kubernetes API. Пока нельзя выставить false"}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "app_version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("2.13.1")}, + }}, + "control_plane_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "sizing_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(50)}, + "count": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)}, + "sizing_policy": schema.StringAttribute{Required: true, MarkdownDescription: "Доступные sizing Policy представлены исходя из ресурсной платформы, которая была выбрана"}, + }}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("sandbox.nubes.ru"), MarkdownDescription: "Облако в котором будет развёрнут тенант"}, + "cluster_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("example-e5uamzll")}, + }}, + "worker_configuration": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VcMgmtSthutrvalClusterResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *VcMgmtSthutrvalClusterModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *VcMgmtSthutrvalClusterModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan VcMgmtSthutrvalClusterModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.WorkerConfiguration.IsNull() && !plan.WorkerConfiguration.IsUnknown() && !state.WorkerConfiguration.IsNull() && !state.WorkerConfiguration.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.WorkerConfiguration.ValueString(), state.WorkerConfiguration.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить worker_configuration", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 771: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString()), "clusterName": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ClusterName.ValueString())}), + 772: resources_core.BuildJSON(map[string]string{"appVersion": fmt.Sprintf("\"%s\"", config.ClusterConfiguration.AppVersion.ValueString())}), + 773: resources_core.BuildJSON(map[string]string{"sizingDisk": fmt.Sprintf("%d", config.ControlPlaneConfiguration.SizingDisk.ValueInt64()), "count": fmt.Sprintf("%d", config.ControlPlaneConfiguration.Count.ValueInt64()), "sizingPolicy": fmt.Sprintf("\"%s\"", config.ControlPlaneConfiguration.SizingPolicy.ValueString())}), + 774: resources_core.FormatString(config.WorkerConfiguration), + 775: resources_core.BuildJSON(map[string]string{"accessIpListAPI": fmt.Sprintf("\"%s\"", config.AccessConfiguration.AccessIpListAPI.ValueString()), "needExternalAddressIngress": fmt.Sprintf("%v", config.AccessConfiguration.NeedExternalAddressIngress.ValueBool()), "accessIpListIngress": fmt.Sprintf("\"%s\"", config.AccessConfiguration.AccessIpListIngress.ValueString()), "needExternalAddressAPI": fmt.Sprintf("%v", config.AccessConfiguration.NeedExternalAddressAPI.ValueBool())}), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 148, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *VcMgmtSthutrvalClusterResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data VcMgmtSthutrvalClusterModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 148, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 771: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString()), "clusterName": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ClusterName.ValueString())}), + 772: resources_core.BuildJSON(map[string]string{"appVersion": fmt.Sprintf("\"%s\"", data.ClusterConfiguration.AppVersion.ValueString())}), + 773: resources_core.BuildJSON(map[string]string{"sizingDisk": fmt.Sprintf("%d", data.ControlPlaneConfiguration.SizingDisk.ValueInt64()), "count": fmt.Sprintf("%d", data.ControlPlaneConfiguration.Count.ValueInt64()), "sizingPolicy": fmt.Sprintf("\"%s\"", data.ControlPlaneConfiguration.SizingPolicy.ValueString())}), + 774: resources_core.FormatString(data.WorkerConfiguration), + 775: resources_core.BuildJSON(map[string]string{"accessIpListAPI": fmt.Sprintf("\"%s\"", data.AccessConfiguration.AccessIpListAPI.ValueString()), "needExternalAddressIngress": fmt.Sprintf("%v", data.AccessConfiguration.NeedExternalAddressIngress.ValueBool()), "accessIpListIngress": fmt.Sprintf("\"%s\"", data.AccessConfiguration.AccessIpListIngress.ValueString()), "needExternalAddressAPI": fmt.Sprintf("%v", data.AccessConfiguration.NeedExternalAddressAPI.ValueBool())}), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 148, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 148, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "controlPlaneConfiguration", Field: "ControlPlaneConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + {Code: "workerConfiguration", Field: "WorkerConfiguration", Type: "array-map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcMgmtSthutrvalClusterResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VcMgmtSthutrvalClusterModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 148, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "controlPlaneConfiguration", Field: "ControlPlaneConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + {Code: "workerConfiguration", Field: "WorkerConfiguration", Type: "array-map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *VcMgmtSthutrvalClusterResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VcMgmtSthutrvalClusterModel + var state VcMgmtSthutrvalClusterModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 148, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "controlPlaneConfiguration", Field: "ControlPlaneConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + {Code: "workerConfiguration", Field: "WorkerConfiguration", Type: "array-map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcMgmtSthutrvalClusterResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VcMgmtSthutrvalClusterModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *VcMgmtSthutrvalClusterResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *VcMgmtSthutrvalClusterResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/149_valo_tenant_resource.go b/generated/dev/go/149_valo_tenant_resource.go new file mode 100644 index 0000000..a3c1a9f --- /dev/null +++ b/generated/dev/go/149_valo_tenant_resource.go @@ -0,0 +1,431 @@ +package resources_gen + +import ( + "context" + "fmt" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: valo_tenant +// Service ID: 149 + +var _ resource.Resource = &ValoTenantResource{} +var _ resource.ResourceWithModifyPlan = &ValoTenantResource{} +var _ resource.ResourceWithImportState = &ValoTenantResource{} + +type ValoTenantResource struct { + client *core.UniversalClient +} + +// ValoTenantValoOptionsModel — вложенная модель для map-fixed параметра valoOptions. +type ValoTenantValoOptionsModel struct { + UserCount types.Int64 `tfsdk:"user_count" json:"userCount"` + RequestApproveMaxPeriod types.Int64 `tfsdk:"request_approve_max_period" json:"requestApproveMaxPeriod"` + FfOnlineEditor types.Bool `tfsdk:"ff_online_editor" json:"ffOnlineEditor"` + FfWatermark types.Bool `tfsdk:"ff_watermark" json:"ffWatermark"` + FfUserImport types.Bool `tfsdk:"ff_user_import" json:"ffUserImport"` + FfThemes types.Bool `tfsdk:"ff_themes" json:"ffThemes"` + FfPublicLink types.Bool `tfsdk:"ff_public_link" json:"ffPublicLink"` + FfLogging types.Bool `tfsdk:"ff_logging" json:"ffLogging"` +} + +type ValoTenantModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + Domain types.String `tfsdk:"domain"` + ResourceRealm types.String `tfsdk:"resource_realm"` + S3Storage types.Int64 `tfsdk:"s3_storage"` + ValoOptions *ValoTenantValoOptionsModel `tfsdk:"valo_options"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewValoTenantResource() resource.Resource { + return &ValoTenantResource{} +} + +func (r *ValoTenantResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_valo_tenant" +} + +func (r *ValoTenantResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "domain": schema.StringAttribute{Required: true, MarkdownDescription: "Будет добавлено в зону valo.ngcloud.ru Не изменяется после создания."}, + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("valo.ngcloud.ru"), MarkdownDescription: "Не изменяется после создания."}, + "s3_storage": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Параметр указывается в `Gb`"}, + "valo_options": schema.SingleNestedAttribute{Optional: true, Computed: true, + Attributes: map[string]schema.Attribute{ + "user_count": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10)}, + "request_approve_max_period": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2), MarkdownDescription: "Максимальное значение периода согласования (в днях)"}, + "ff_online_editor": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Тарифная опция"}, + "ff_watermark": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Тарифная опция"}, + "ff_user_import": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Тарифная опция"}, + "ff_themes": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Тарифная опция"}, + "ff_public_link": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Тарифная опция"}, + "ff_logging": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Тарифная опция"}, + }}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *ValoTenantResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *ValoTenantModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *ValoTenantModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan ValoTenantModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.Domain.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "domain is required.") + return + } + if config.Domain.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 655: resources_core.FormatString(config.ResourceRealm), + 656: resources_core.FormatString(config.Domain), + 657: resources_core.BuildJSON(map[string]string{"userCount": fmt.Sprintf("%d", config.ValoOptions.UserCount.ValueInt64()), "requestApproveMaxPeriod": fmt.Sprintf("%d", config.ValoOptions.RequestApproveMaxPeriod.ValueInt64()), "ffOnlineEditor": fmt.Sprintf("%v", config.ValoOptions.FfOnlineEditor.ValueBool()), "ffWatermark": fmt.Sprintf("%v", config.ValoOptions.FfWatermark.ValueBool()), "ffUserImport": fmt.Sprintf("%v", config.ValoOptions.FfUserImport.ValueBool()), "ffThemes": fmt.Sprintf("%v", config.ValoOptions.FfThemes.ValueBool()), "ffPublicLink": fmt.Sprintf("%v", config.ValoOptions.FfPublicLink.ValueBool()), "ffLogging": fmt.Sprintf("%v", config.ValoOptions.FfLogging.ValueBool())}), + 658: resources_core.FormatInt64(config.S3Storage), + } + desiredDomain := "" + if !config.Domain.IsNull() && !config.Domain.IsUnknown() { + desiredDomain = config.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 149, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *ValoTenantResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data ValoTenantModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.Domain.IsNull() || data.Domain.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "domain is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + if !data.Domain.IsNull() && !data.Domain.IsUnknown() { + desiredDomain = data.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 149, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 655: resources_core.FormatString(data.ResourceRealm), + 656: resources_core.FormatString(data.Domain), + 657: resources_core.BuildJSON(map[string]string{"userCount": fmt.Sprintf("%d", data.ValoOptions.UserCount.ValueInt64()), "requestApproveMaxPeriod": fmt.Sprintf("%d", data.ValoOptions.RequestApproveMaxPeriod.ValueInt64()), "ffOnlineEditor": fmt.Sprintf("%v", data.ValoOptions.FfOnlineEditor.ValueBool()), "ffWatermark": fmt.Sprintf("%v", data.ValoOptions.FfWatermark.ValueBool()), "ffUserImport": fmt.Sprintf("%v", data.ValoOptions.FfUserImport.ValueBool()), "ffThemes": fmt.Sprintf("%v", data.ValoOptions.FfThemes.ValueBool()), "ffPublicLink": fmt.Sprintf("%v", data.ValoOptions.FfPublicLink.ValueBool()), "ffLogging": fmt.Sprintf("%v", data.ValoOptions.FfLogging.ValueBool())}), + 658: resources_core.FormatInt64(data.S3Storage), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 149, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 149, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "s3Storage", Field: "S3Storage", Type: "int64"}, + {Code: "valoOptions", Field: "ValoOptions", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *ValoTenantResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state ValoTenantModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 149, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "s3Storage", Field: "S3Storage", Type: "int64"}, + {Code: "valoOptions", Field: "ValoOptions", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *ValoTenantResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan ValoTenantModel + var state ValoTenantModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.S3Storage.IsNull() != state.S3Storage.IsNull() || plan.S3Storage.IsUnknown() != state.S3Storage.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.S3Storage.IsNull() && !plan.S3Storage.IsUnknown() && !state.S3Storage.IsNull() && !state.S3Storage.IsUnknown() { + if plan.S3Storage.ValueInt64() != state.S3Storage.ValueInt64() { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 659: resources_core.BuildJSON(map[string]string{"ffUserImport": fmt.Sprintf("%v", plan.ValoOptions.FfUserImport.ValueBool()), "ffThemes": fmt.Sprintf("%v", plan.ValoOptions.FfThemes.ValueBool()), "ffPublicLink": fmt.Sprintf("%v", plan.ValoOptions.FfPublicLink.ValueBool()), "ffLogging": fmt.Sprintf("%v", plan.ValoOptions.FfLogging.ValueBool()), "userCount": fmt.Sprintf("%d", plan.ValoOptions.UserCount.ValueInt64()), "requestApproveMaxPeriod": fmt.Sprintf("%d", plan.ValoOptions.RequestApproveMaxPeriod.ValueInt64()), "ffOnlineEditor": fmt.Sprintf("%v", plan.ValoOptions.FfOnlineEditor.ValueBool()), "ffWatermark": fmt.Sprintf("%v", plan.ValoOptions.FfWatermark.ValueBool())}), + 660: resources_core.FormatInt64(plan.S3Storage), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 149, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "s3Storage", Field: "S3Storage", Type: "int64"}, + {Code: "valoOptions", Field: "ValoOptions", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *ValoTenantResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state ValoTenantModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *ValoTenantResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *ValoTenantResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/150_k8s_sthutrval_cluster_resource.go b/generated/dev/go/150_k8s_sthutrval_cluster_resource.go new file mode 100644 index 0000000..97d9772 --- /dev/null +++ b/generated/dev/go/150_k8s_sthutrval_cluster_resource.go @@ -0,0 +1,429 @@ +package resources_gen + +import ( + "context" + "fmt" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: k8s_sthutrval_cluster +// Service ID: 150 + +var _ resource.Resource = &K8sSthutrvalClusterResource{} +var _ resource.ResourceWithModifyPlan = &K8sSthutrvalClusterResource{} +var _ resource.ResourceWithImportState = &K8sSthutrvalClusterResource{} + +type K8sSthutrvalClusterResource struct { + client *core.UniversalClient +} + +// K8sSthutrvalClusterAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration. +type K8sSthutrvalClusterAccessConfigurationModel struct { + NeedExternalAddressIngress types.Bool `tfsdk:"need_external_address_ingress" json:"needExternalAddressIngress"` + AccessIpListIngress types.String `tfsdk:"access_ip_list_ingress" json:"accessIpListIngress"` + NeedExternalAddressAPI types.Bool `tfsdk:"need_external_address_a_p_i" json:"needExternalAddressAPI"` + AccessIpListAPI types.String `tfsdk:"access_ip_list_a_p_i" json:"accessIpListAPI"` +} + +// K8sSthutrvalClusterClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type K8sSthutrvalClusterClusterConfigurationModel struct { + AppVersion types.String `tfsdk:"app_version" json:"appVersion"` +} + +// K8sSthutrvalClusterControlPlaneConfigurationModel — вложенная модель для map-fixed параметра controlPlaneConfiguration. +type K8sSthutrvalClusterControlPlaneConfigurationModel struct { + SizingDisk types.Int64 `tfsdk:"sizing_disk" json:"sizingDisk"` + Count types.Int64 `tfsdk:"count" json:"count"` + SizingPolicy types.String `tfsdk:"sizing_policy" json:"sizingPolicy"` +} + +// K8sSthutrvalClusterStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type K8sSthutrvalClusterStartupConfigurationModel struct { + ClusterName types.String `tfsdk:"cluster_name" json:"clusterName"` + VdcUid types.String `tfsdk:"vdc_uid" json:"vdcUid"` + NsxtUid types.String `tfsdk:"nsxt_uid" json:"nsxtUid"` +} + +type K8sSthutrvalClusterModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessConfiguration *K8sSthutrvalClusterAccessConfigurationModel `tfsdk:"access_configuration"` + ClusterConfiguration *K8sSthutrvalClusterClusterConfigurationModel `tfsdk:"cluster_configuration"` + ControlPlaneConfiguration *K8sSthutrvalClusterControlPlaneConfigurationModel `tfsdk:"control_plane_configuration"` + StartupConfiguration *K8sSthutrvalClusterStartupConfigurationModel `tfsdk:"startup_configuration"` + WorkerConfiguration types.String `tfsdk:"worker_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewK8sSthutrvalClusterResource() resource.Resource { + return &K8sSthutrvalClusterResource{} +} + +func (r *K8sSthutrvalClusterResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_k8s_sthutrval_cluster" +} + +func (r *K8sSthutrvalClusterResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "need_external_address_ingress": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Выделяет внешний IP для доступа к Ingress"}, + "access_ip_list_ingress": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Access IP List для подключения к Ingress. Указывается массив. Если передан пустой массив, доступ выделяется всем"}, + "need_external_address_a_p_i": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Выделяет внешний IP для доступа к kubernetes API. Пока нельзя выставить false"}, + "access_ip_list_a_p_i": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Access IP List для подключения к Kubernetes API. Указывается массив. Если передан пустой массив, доступ выделяется всем"}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "app_version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("2.13.1")}, + }}, + "control_plane_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "sizing_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(50), MarkdownDescription: "Дисковая политика берётся из услуги VDC, первый в списке"}, + "count": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)}, + "sizing_policy": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Доступные sizing Policy можно обновлять через услугу `Виртуальный датацентр`"}, + }}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "cluster_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("example-vlzeyhxy")}, + "vdc_uid": schema.StringAttribute{Required: true}, + "nsxt_uid": schema.StringAttribute{Required: true}, + }}, + "worker_configuration": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]")}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *K8sSthutrvalClusterResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *K8sSthutrvalClusterModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *K8sSthutrvalClusterModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan K8sSthutrvalClusterModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 750: resources_core.BuildJSON(map[string]string{"clusterName": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ClusterName.ValueString()), "vdcUid": fmt.Sprintf("\"%s\"", config.StartupConfiguration.VdcUid.ValueString()), "nsxtUid": fmt.Sprintf("\"%s\"", config.StartupConfiguration.NsxtUid.ValueString())}), + 751: resources_core.BuildJSON(map[string]string{"sizingDisk": fmt.Sprintf("%d", config.ControlPlaneConfiguration.SizingDisk.ValueInt64()), "count": fmt.Sprintf("%d", config.ControlPlaneConfiguration.Count.ValueInt64()), "sizingPolicy": fmt.Sprintf("\"%s\"", config.ControlPlaneConfiguration.SizingPolicy.ValueString())}), + 752: resources_core.FormatString(config.WorkerConfiguration), + 753: resources_core.BuildJSON(map[string]string{"needExternalAddressIngress": fmt.Sprintf("%v", config.AccessConfiguration.NeedExternalAddressIngress.ValueBool()), "accessIpListIngress": fmt.Sprintf("\"%s\"", config.AccessConfiguration.AccessIpListIngress.ValueString()), "needExternalAddressAPI": fmt.Sprintf("%v", config.AccessConfiguration.NeedExternalAddressAPI.ValueBool()), "accessIpListAPI": fmt.Sprintf("\"%s\"", config.AccessConfiguration.AccessIpListAPI.ValueString())}), + 755: resources_core.BuildJSON(map[string]string{"appVersion": fmt.Sprintf("\"%s\"", config.ClusterConfiguration.AppVersion.ValueString())}), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 150, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *K8sSthutrvalClusterResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data K8sSthutrvalClusterModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 150, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 750: resources_core.BuildJSON(map[string]string{"clusterName": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ClusterName.ValueString()), "vdcUid": fmt.Sprintf("\"%s\"", data.StartupConfiguration.VdcUid.ValueString()), "nsxtUid": fmt.Sprintf("\"%s\"", data.StartupConfiguration.NsxtUid.ValueString())}), + 751: resources_core.BuildJSON(map[string]string{"sizingDisk": fmt.Sprintf("%d", data.ControlPlaneConfiguration.SizingDisk.ValueInt64()), "count": fmt.Sprintf("%d", data.ControlPlaneConfiguration.Count.ValueInt64()), "sizingPolicy": fmt.Sprintf("\"%s\"", data.ControlPlaneConfiguration.SizingPolicy.ValueString())}), + 752: resources_core.FormatString(data.WorkerConfiguration), + 753: resources_core.BuildJSON(map[string]string{"needExternalAddressIngress": fmt.Sprintf("%v", data.AccessConfiguration.NeedExternalAddressIngress.ValueBool()), "accessIpListIngress": fmt.Sprintf("\"%s\"", data.AccessConfiguration.AccessIpListIngress.ValueString()), "needExternalAddressAPI": fmt.Sprintf("%v", data.AccessConfiguration.NeedExternalAddressAPI.ValueBool()), "accessIpListAPI": fmt.Sprintf("\"%s\"", data.AccessConfiguration.AccessIpListAPI.ValueString())}), + 755: resources_core.BuildJSON(map[string]string{"appVersion": fmt.Sprintf("\"%s\"", data.ClusterConfiguration.AppVersion.ValueString())}), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 150, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 150, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "controlPlaneConfiguration", Field: "ControlPlaneConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + {Code: "workerConfiguration", Field: "WorkerConfiguration", Type: "array-map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *K8sSthutrvalClusterResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state K8sSthutrvalClusterModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 150, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "controlPlaneConfiguration", Field: "ControlPlaneConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + {Code: "workerConfiguration", Field: "WorkerConfiguration", Type: "array-map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *K8sSthutrvalClusterResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan K8sSthutrvalClusterModel + var state K8sSthutrvalClusterModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.WorkerConfiguration.IsNull() != state.WorkerConfiguration.IsNull() || plan.WorkerConfiguration.IsUnknown() != state.WorkerConfiguration.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.WorkerConfiguration.IsNull() && !plan.WorkerConfiguration.IsUnknown() && !state.WorkerConfiguration.IsNull() && !state.WorkerConfiguration.IsUnknown() { + if !strings.EqualFold(plan.WorkerConfiguration.ValueString(), state.WorkerConfiguration.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 756: resources_core.BuildJSON(map[string]string{"appVersion": fmt.Sprintf("\"%s\"", plan.ClusterConfiguration.AppVersion.ValueString())}), + 757: resources_core.BuildJSON(map[string]string{"sizingPolicy": fmt.Sprintf("\"%s\"", plan.ControlPlaneConfiguration.SizingPolicy.ValueString()), "sizingDisk": fmt.Sprintf("%d", plan.ControlPlaneConfiguration.SizingDisk.ValueInt64()), "count": fmt.Sprintf("%d", plan.ControlPlaneConfiguration.Count.ValueInt64())}), + 758: resources_core.FormatString(plan.WorkerConfiguration), + 759: resources_core.BuildJSON(map[string]string{"needExternalAddressIngress": fmt.Sprintf("%v", plan.AccessConfiguration.NeedExternalAddressIngress.ValueBool()), "accessIpListIngress": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.AccessIpListIngress.ValueString()), "needExternalAddressAPI": fmt.Sprintf("%v", plan.AccessConfiguration.NeedExternalAddressAPI.ValueBool()), "accessIpListAPI": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.AccessIpListAPI.ValueString())}), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 150, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "controlPlaneConfiguration", Field: "ControlPlaneConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + {Code: "workerConfiguration", Field: "WorkerConfiguration", Type: "array-map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *K8sSthutrvalClusterResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state K8sSthutrvalClusterModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *K8sSthutrvalClusterResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *K8sSthutrvalClusterResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/150_k8s_sthutrval_cluster_user_resource.go b/generated/dev/go/150_k8s_sthutrval_cluster_user_resource.go new file mode 100644 index 0000000..55faf0e --- /dev/null +++ b/generated/dev/go/150_k8s_sthutrval_cluster_user_resource.go @@ -0,0 +1,343 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: k8s_sthutrval_cluster +// Subresource: user + +var _ resource.Resource = &K8sSthutrvalClusterUserResource{} + +// K8sSthutrvalClusterUserResource manages a subresource via operations. +type K8sSthutrvalClusterUserResource struct { + client *core.UniversalClient +} + +type K8sSthutrvalClusterUserModel struct { + ID types.String `tfsdk:"id"` + K8sSthutrvalClusterID types.String `tfsdk:"k8s_sthutrval_cluster_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + Admins types.String `tfsdk:"admins"` +} + +func NewK8sSthutrvalClusterUserResource() resource.Resource { + return &K8sSthutrvalClusterUserResource{} +} + +func (r *K8sSthutrvalClusterUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_k8s_sthutrval_cluster_user" +} + +func (r *K8sSthutrvalClusterUserResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "k8s_sthutrval_cluster_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "admins": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *K8sSthutrvalClusterUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan K8sSthutrvalClusterUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.K8sSthutrvalClusterID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "admins": resources_core.FormatString(plan.Admins), + } + identityCodes := []string{ + "admins", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "admins": resources_core.FormatString(plan.Admins), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *K8sSthutrvalClusterUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state K8sSthutrvalClusterUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *K8sSthutrvalClusterUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan K8sSthutrvalClusterUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state K8sSthutrvalClusterUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.K8sSthutrvalClusterID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{ + "admins": resources_core.FormatString(plan.Admins), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *K8sSthutrvalClusterUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state K8sSthutrvalClusterUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_user" == "" { + return + } + + instanceUID := strings.TrimSpace(state.K8sSthutrvalClusterID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "admins": resources_core.FormatString(state.Admins), + } + identityCodes := []string{ + "admins", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "admins": resources_core.FormatString(state.Admins), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_user", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *K8sSthutrvalClusterUserResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/151_k8s_openbao_kv_resource.go b/generated/dev/go/151_k8s_openbao_kv_resource.go new file mode 100644 index 0000000..773a5c2 --- /dev/null +++ b/generated/dev/go/151_k8s_openbao_kv_resource.go @@ -0,0 +1,343 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: k8s_openbao +// Subresource: kv + +var _ resource.Resource = &K8sOpenbaoKvResource{} + +// K8sOpenbaoKvResource manages a subresource via operations. +type K8sOpenbaoKvResource struct { + client *core.UniversalClient +} + +type K8sOpenbaoKvModel struct { + ID types.String `tfsdk:"id"` + K8sOpenbaoID types.String `tfsdk:"k8s_openbao_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + KvName types.String `tfsdk:"kv_name"` +} + +func NewK8sOpenbaoKvResource() resource.Resource { + return &K8sOpenbaoKvResource{} +} + +func (r *K8sOpenbaoKvResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_k8s_openbao_kv" +} + +func (r *K8sOpenbaoKvResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "k8s_openbao_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "kv_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("kvname-ldnwybqu"), MarkdownDescription: "Будет создан `v2 тип движка`

Уникальное в рамках услуги Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *K8sOpenbaoKvResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan K8sOpenbaoKvModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.K8sOpenbaoID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "kvName": resources_core.FormatString(plan.KvName), + } + identityCodes := []string{ + "kvName", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("kv", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "kv", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "kvName": resources_core.FormatString(plan.KvName), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_kv", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_kv", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "kv", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "kv", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "kv", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *K8sOpenbaoKvResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state K8sOpenbaoKvModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *K8sOpenbaoKvResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan K8sOpenbaoKvModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state K8sOpenbaoKvModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.K8sOpenbaoID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{ + "kvName": resources_core.FormatString(plan.KvName), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "kv", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *K8sOpenbaoKvResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state K8sOpenbaoKvModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_kv" == "" { + return + } + + instanceUID := strings.TrimSpace(state.K8sOpenbaoID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "kvName": resources_core.FormatString(state.KvName), + } + identityCodes := []string{ + "kvName", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("kv", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "kvName": resources_core.FormatString(state.KvName), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_kv", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *K8sOpenbaoKvResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/151_k8s_openbao_policy_resource.go b/generated/dev/go/151_k8s_openbao_policy_resource.go new file mode 100644 index 0000000..668ed29 --- /dev/null +++ b/generated/dev/go/151_k8s_openbao_policy_resource.go @@ -0,0 +1,355 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: k8s_openbao +// Subresource: policy + +var _ resource.Resource = &K8sOpenbaoPolicyResource{} + +// K8sOpenbaoPolicyResource manages a subresource via operations. +type K8sOpenbaoPolicyResource struct { + client *core.UniversalClient +} + +type K8sOpenbaoPolicyModel struct { + ID types.String `tfsdk:"id"` + K8sOpenbaoID types.String `tfsdk:"k8s_openbao_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + PolicyName types.String `tfsdk:"policy_name"` + PolicySettings types.String `tfsdk:"policy_settings"` +} + +func NewK8sOpenbaoPolicyResource() resource.Resource { + return &K8sOpenbaoPolicyResource{} +} + +func (r *K8sOpenbaoPolicyResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_k8s_openbao_policy" +} + +func (r *K8sOpenbaoPolicyResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "k8s_openbao_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "policy_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("policy-re1w8vbp"), MarkdownDescription: "Сделано для дальнейшего удобного использования Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "policy_settings": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *K8sOpenbaoPolicyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan K8sOpenbaoPolicyModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if plan.PolicySettings.IsNull() || plan.PolicySettings.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "policy_settings is required.") + return + } + + instanceUID := strings.TrimSpace(plan.K8sOpenbaoID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "policyName": resources_core.FormatString(plan.PolicyName), + "policySettings": resources_core.FormatString(plan.PolicySettings), + } + identityCodes := []string{ + "policyName", + "policySettings", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("policy", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "policy", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "policySettings": resources_core.FormatString(plan.PolicySettings), + "policyName": resources_core.FormatString(plan.PolicyName), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_policy", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_policy", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "policy", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "policy", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "policy", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *K8sOpenbaoPolicyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state K8sOpenbaoPolicyModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *K8sOpenbaoPolicyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan K8sOpenbaoPolicyModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state K8sOpenbaoPolicyModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.K8sOpenbaoID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{ + "policyName": resources_core.FormatString(plan.PolicyName), + "policySettings": resources_core.FormatString(plan.PolicySettings), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "policy", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *K8sOpenbaoPolicyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state K8sOpenbaoPolicyModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_policy" == "" { + return + } + + instanceUID := strings.TrimSpace(state.K8sOpenbaoID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "policyName": resources_core.FormatString(state.PolicyName), + "policySettings": resources_core.FormatString(state.PolicySettings), + } + identityCodes := []string{ + "policyName", + "policySettings", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("policy", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "policyName": resources_core.FormatString(state.PolicyName), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_policy", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *K8sOpenbaoPolicyResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/151_k8s_openbao_resource.go b/generated/dev/go/151_k8s_openbao_resource.go new file mode 100644 index 0000000..821648e --- /dev/null +++ b/generated/dev/go/151_k8s_openbao_resource.go @@ -0,0 +1,468 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: k8s_openbao +// Service ID: 151 + +var _ resource.Resource = &K8sOpenbaoResource{} +var _ resource.ResourceWithModifyPlan = &K8sOpenbaoResource{} +var _ resource.ResourceWithImportState = &K8sOpenbaoResource{} + +type K8sOpenbaoResource struct { + client *core.UniversalClient +} + +type K8sOpenbaoModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AppVersion types.String `tfsdk:"app_version"` + Domain types.String `tfsdk:"domain"` + ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"` + ResourceDisk types.Int64 `tfsdk:"resource_disk"` + ResourceInstances types.Int64 `tfsdk:"resource_instances"` + ResourceMemory types.Int64 `tfsdk:"resource_memory"` + ResourceRealm types.String `tfsdk:"resource_realm"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewK8sOpenbaoResource() resource.Resource { + return &K8sOpenbaoResource{} +} + +func (r *K8sOpenbaoResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_k8s_openbao" +} + +func (r *K8sOpenbaoResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "app_version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("2.5.2"), MarkdownDescription: "Выбрать из списка"}, + "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("bao-pasczlik"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."}, + "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Пример: `500`"}, + "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Пример: `10`"}, + "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(3), MarkdownDescription: "Пример: `3`"}, + "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Пример: `2048`"}, + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes в котором будет развёрнуто приложение Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *K8sOpenbaoResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *K8sOpenbaoModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *K8sOpenbaoModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan K8sOpenbaoModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.ResourceRealm.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + return + } + if config.ResourceRealm.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 663: resources_core.FormatString(config.ResourceRealm), + 664: resources_core.FormatInt64(config.ResourceCPU), + 665: resources_core.FormatInt64(config.ResourceInstances), + 666: resources_core.FormatInt64(config.ResourceMemory), + 667: resources_core.FormatInt64(config.ResourceDisk), + 668: resources_core.FormatString(config.AppVersion), + 669: resources_core.FormatString(config.Domain), + } + desiredDomain := "" + if !config.Domain.IsNull() && !config.Domain.IsUnknown() { + desiredDomain = config.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 151, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *K8sOpenbaoResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data K8sOpenbaoModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + if !data.Domain.IsNull() && !data.Domain.IsUnknown() { + desiredDomain = data.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 151, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 663: resources_core.FormatString(data.ResourceRealm), + 664: resources_core.FormatInt64(data.ResourceCPU), + 665: resources_core.FormatInt64(data.ResourceInstances), + 666: resources_core.FormatInt64(data.ResourceMemory), + 667: resources_core.FormatInt64(data.ResourceDisk), + 668: resources_core.FormatString(data.AppVersion), + 669: resources_core.FormatString(data.Domain), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 151, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 151, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "appVersion", Field: "AppVersion", Type: "string"}, + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *K8sOpenbaoResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state K8sOpenbaoModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 151, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "appVersion", Field: "AppVersion", Type: "string"}, + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *K8sOpenbaoResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan K8sOpenbaoModel + var state K8sOpenbaoModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.ResourceInstances.IsNull() != state.ResourceInstances.IsNull() || plan.ResourceInstances.IsUnknown() != state.ResourceInstances.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() { + if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceCPU.IsNull() != state.ResourceCPU.IsNull() || plan.ResourceCPU.IsUnknown() != state.ResourceCPU.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() { + if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceMemory.IsNull() != state.ResourceMemory.IsNull() || plan.ResourceMemory.IsUnknown() != state.ResourceMemory.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() { + if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceDisk.IsNull() != state.ResourceDisk.IsNull() || plan.ResourceDisk.IsUnknown() != state.ResourceDisk.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() { + if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.AppVersion.IsNull() != state.AppVersion.IsNull() || plan.AppVersion.IsUnknown() != state.AppVersion.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.AppVersion.IsNull() && !plan.AppVersion.IsUnknown() && !state.AppVersion.IsNull() && !state.AppVersion.IsUnknown() { + if !strings.EqualFold(plan.AppVersion.ValueString(), state.AppVersion.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 670: resources_core.FormatInt64(plan.ResourceInstances), + 671: resources_core.FormatInt64(plan.ResourceCPU), + 672: resources_core.FormatInt64(plan.ResourceMemory), + 673: resources_core.FormatInt64(plan.ResourceDisk), + 674: resources_core.FormatString(plan.AppVersion), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 151, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "appVersion", Field: "AppVersion", Type: "string"}, + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *K8sOpenbaoResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state K8sOpenbaoModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *K8sOpenbaoResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *K8sOpenbaoResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/151_k8s_openbao_secrets_resource.go b/generated/dev/go/151_k8s_openbao_secrets_resource.go new file mode 100644 index 0000000..161164e --- /dev/null +++ b/generated/dev/go/151_k8s_openbao_secrets_resource.go @@ -0,0 +1,368 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: k8s_openbao +// Subresource: secrets + +var _ resource.Resource = &K8sOpenbaoSecretsResource{} + +// K8sOpenbaoSecretsResource manages a subresource via operations. +type K8sOpenbaoSecretsResource struct { + client *core.UniversalClient +} + +type K8sOpenbaoSecretsModel struct { + ID types.String `tfsdk:"id"` + K8sOpenbaoID types.String `tfsdk:"k8s_openbao_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + KvName types.String `tfsdk:"kv_name"` + SecretData types.String `tfsdk:"secret_data"` + SecretPath types.String `tfsdk:"secret_path"` +} + +func NewK8sOpenbaoSecretsResource() resource.Resource { + return &K8sOpenbaoSecretsResource{} +} + +func (r *K8sOpenbaoSecretsResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_k8s_openbao_secrets" +} + +func (r *K8sOpenbaoSecretsResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "k8s_openbao_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "kv_name": schema.StringAttribute{Required: true, MarkdownDescription: "Выберите из списка Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "secret_data": schema.StringAttribute{Required: true, MarkdownDescription: "Хотя бы один секрет необходимо добавить Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "secret_path": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("path/to/mysecret"), MarkdownDescription: "Пример: `path` или `road/to/path` Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *K8sOpenbaoSecretsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan K8sOpenbaoSecretsModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if plan.KvName.IsNull() || plan.KvName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "kv_name is required.") + return + } + if plan.SecretData.IsNull() || plan.SecretData.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "secret_data is required.") + return + } + + instanceUID := strings.TrimSpace(plan.K8sOpenbaoID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "kvName": resources_core.FormatString(plan.KvName), + "secretData": resources_core.FormatString(plan.SecretData), + "secretPath": resources_core.FormatString(plan.SecretPath), + } + identityCodes := []string{ + "kvName", + "secretData", + "secretPath", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("secrets", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "secrets", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "kvName": resources_core.FormatString(plan.KvName), + "secretPath": resources_core.FormatString(plan.SecretPath), + "secretData": resources_core.FormatString(plan.SecretData), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_secrets", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_secrets", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "secrets", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "secrets", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "secrets", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *K8sOpenbaoSecretsResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state K8sOpenbaoSecretsModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *K8sOpenbaoSecretsResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan K8sOpenbaoSecretsModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state K8sOpenbaoSecretsModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.K8sOpenbaoID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{ + "kvName": resources_core.FormatString(plan.KvName), + "secretData": resources_core.FormatString(plan.SecretData), + "secretPath": resources_core.FormatString(plan.SecretPath), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "secrets", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *K8sOpenbaoSecretsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state K8sOpenbaoSecretsModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_secrets" == "" { + return + } + + instanceUID := strings.TrimSpace(state.K8sOpenbaoID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "kvName": resources_core.FormatString(state.KvName), + "secretData": resources_core.FormatString(state.SecretData), + "secretPath": resources_core.FormatString(state.SecretPath), + } + identityCodes := []string{ + "kvName", + "secretData", + "secretPath", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("secrets", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "kvName": resources_core.FormatString(state.KvName), + "secretPath": resources_core.FormatString(state.SecretPath), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_secrets", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *K8sOpenbaoSecretsResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/153_nifi_resource.go b/generated/dev/go/153_nifi_resource.go new file mode 100644 index 0000000..12049ba --- /dev/null +++ b/generated/dev/go/153_nifi_resource.go @@ -0,0 +1,474 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: nifi +// Service ID: 153 + +var _ resource.Resource = &NifiResource{} +var _ resource.ResourceWithModifyPlan = &NifiResource{} +var _ resource.ResourceWithImportState = &NifiResource{} + +type NifiResource struct { + client *core.UniversalClient +} + +type NifiModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AppVersion types.String `tfsdk:"app_version"` + Domain types.String `tfsdk:"domain"` + JdbcDrivers types.String `tfsdk:"jdbc_drivers"` + ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"` + ResourceDisk types.Int64 `tfsdk:"resource_disk"` + ResourceInstances types.Int64 `tfsdk:"resource_instances"` + ResourceMemory types.Int64 `tfsdk:"resource_memory"` + ResourceRealm types.String `tfsdk:"resource_realm"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewNifiResource() resource.Resource { + return &NifiResource{} +} + +func (r *NifiResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_nifi" +} + +func (r *NifiResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "app_version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("2.6.0"), MarkdownDescription: "Выбрать из списка Не изменяется после создания."}, + "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("nifi-lvu4xj7j"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."}, + "jdbc_drivers": schema.StringAttribute{Required: true, MarkdownDescription: "Пример: `https://jdbc.postgresql.org/download/postgresql-42.7.8.jar` Не изменяется после создания."}, + "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2000), MarkdownDescription: "Пример: `2000` Не изменяется после создания."}, + "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Пример: `10` Не изменяется после создания."}, + "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Пример: `1` Не изменяется после создания."}, + "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(4096), MarkdownDescription: "Пример: `2048` Не изменяется после создания."}, + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes в котором будет развёрнуто приложение Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *NifiResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *NifiModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *NifiModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan NifiModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.AppVersion.IsNull() && !plan.AppVersion.IsUnknown() && !state.AppVersion.IsNull() && !state.AppVersion.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.AppVersion.ValueString(), state.AppVersion.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить app_version", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.JdbcDrivers.IsNull() && !plan.JdbcDrivers.IsUnknown() && !state.JdbcDrivers.IsNull() && !state.JdbcDrivers.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.JdbcDrivers.ValueString(), state.JdbcDrivers.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить jdbc_drivers", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() { + if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_c_p_u", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() { + if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_disk", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() { + if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_instances", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() { + if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_memory", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.JdbcDrivers.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "jdbc_drivers is required.") + return + } + if config.JdbcDrivers.IsUnknown() { + return + } + if config.ResourceRealm.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + return + } + if config.ResourceRealm.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 711: resources_core.FormatString(config.Domain), + 712: resources_core.FormatInt64(config.ResourceCPU), + 713: resources_core.FormatInt64(config.ResourceMemory), + 714: resources_core.FormatInt64(config.ResourceInstances), + 715: resources_core.FormatInt64(config.ResourceDisk), + 716: resources_core.FormatString(config.AppVersion), + 717: resources_core.FormatString(config.ResourceRealm), + 718: resources_core.FormatString(config.JdbcDrivers), + } + desiredDomain := "" + if !config.Domain.IsNull() && !config.Domain.IsUnknown() { + desiredDomain = config.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 153, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *NifiResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data NifiModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.JdbcDrivers.IsNull() || data.JdbcDrivers.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "jdbc_drivers is required.") + return + } + if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + if !data.Domain.IsNull() && !data.Domain.IsUnknown() { + desiredDomain = data.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 153, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 711: resources_core.FormatString(data.Domain), + 712: resources_core.FormatInt64(data.ResourceCPU), + 713: resources_core.FormatInt64(data.ResourceMemory), + 714: resources_core.FormatInt64(data.ResourceInstances), + 715: resources_core.FormatInt64(data.ResourceDisk), + 716: resources_core.FormatString(data.AppVersion), + 717: resources_core.FormatString(data.ResourceRealm), + 718: resources_core.FormatString(data.JdbcDrivers), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 153, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 153, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "appVersion", Field: "AppVersion", Type: "string"}, + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "jdbcDrivers", Field: "JdbcDrivers", Type: "array-map-fixed"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *NifiResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state NifiModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 153, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "appVersion", Field: "AppVersion", Type: "string"}, + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "jdbcDrivers", Field: "JdbcDrivers", Type: "array-map-fixed"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *NifiResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan NifiModel + var state NifiModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 153, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "appVersion", Field: "AppVersion", Type: "string"}, + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "jdbcDrivers", Field: "JdbcDrivers", Type: "array-map-fixed"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *NifiResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state NifiModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *NifiResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *NifiResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/163_llm_ai_resource.go b/generated/dev/go/163_llm_ai_resource.go new file mode 100644 index 0000000..22a1d70 --- /dev/null +++ b/generated/dev/go/163_llm_ai_resource.go @@ -0,0 +1,345 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: llm_ai +// Service ID: 163 + +var _ resource.Resource = &LlmAiResource{} +var _ resource.ResourceWithModifyPlan = &LlmAiResource{} +var _ resource.ResourceWithImportState = &LlmAiResource{} + +type LlmAiResource struct { + client *core.UniversalClient +} + +type LlmAiModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + ResourceRealm types.String `tfsdk:"resource_realm"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewLlmAiResource() resource.Resource { + return &LlmAiResource{} +} + +func (r *LlmAiResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_llm_ai" +} + +func (r *LlmAiResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("llmAi.aillm.ru"), MarkdownDescription: "Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *LlmAiResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *LlmAiModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *LlmAiModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan LlmAiModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 826: resources_core.FormatString(config.ResourceRealm), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 163, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *LlmAiResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data LlmAiModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 163, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 826: resources_core.FormatString(data.ResourceRealm), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 163, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 163, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *LlmAiResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state LlmAiModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 163, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *LlmAiResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan LlmAiModel + var state LlmAiModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 163, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *LlmAiResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state LlmAiModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *LlmAiResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *LlmAiResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/163_llm_ai_secrets_resource.go b/generated/dev/go/163_llm_ai_secrets_resource.go new file mode 100644 index 0000000..0525342 --- /dev/null +++ b/generated/dev/go/163_llm_ai_secrets_resource.go @@ -0,0 +1,401 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: llm_ai +// Subresource: secrets + +var _ resource.Resource = &LlmAiSecretsResource{} + +// LlmAiSecretsResource manages a subresource via operations. +type LlmAiSecretsResource struct { + client *core.UniversalClient +} + +type LlmAiSecretsModel struct { + ID types.String `tfsdk:"id"` + LlmAiID types.String `tfsdk:"llm_ai_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + KeyName types.String `tfsdk:"key_name"` + Models types.String `tfsdk:"models"` + RpmLimit types.Int64 `tfsdk:"rpm_limit"` + TeamName types.String `tfsdk:"team_name"` + TpmLimit types.Int64 `tfsdk:"tpm_limit"` + VariablesKeys types.String `tfsdk:"variables_keys"` +} + +func NewLlmAiSecretsResource() resource.Resource { + return &LlmAiSecretsResource{} +} + +func (r *LlmAiSecretsResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_llm_ai_secrets" +} + +func (r *LlmAiSecretsResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "llm_ai_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "key_name": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "models": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "rpm_limit": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(60), MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.Int64{int64planmodifier.RequiresReplace()}}, + "team_name": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "tpm_limit": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100000), MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.Int64{int64planmodifier.RequiresReplace()}}, + "variables_keys": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *LlmAiSecretsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan LlmAiSecretsModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if plan.KeyName.IsNull() || plan.KeyName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "key_name is required.") + return + } + if plan.Models.IsNull() || plan.Models.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "models is required.") + return + } + if plan.TeamName.IsNull() || plan.TeamName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "team_name is required.") + return + } + if plan.VariablesKeys.IsNull() || plan.VariablesKeys.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "variables_keys is required.") + return + } + + instanceUID := strings.TrimSpace(plan.LlmAiID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "keyName": resources_core.FormatString(plan.KeyName), + "models": resources_core.FormatString(plan.Models), + "rpmLimit": resources_core.FormatInt64(plan.RpmLimit), + "teamName": resources_core.FormatString(plan.TeamName), + "tpmLimit": resources_core.FormatInt64(plan.TpmLimit), + "variablesKeys": resources_core.FormatString(plan.VariablesKeys), + } + identityCodes := []string{ + "keyName", + "models", + "rpmLimit", + "teamName", + "tpmLimit", + "variablesKeys", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("secrets", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "secrets", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "models": resources_core.FormatString(plan.Models), + "rpmLimit": resources_core.FormatInt64(plan.RpmLimit), + "tpmLimit": resources_core.FormatInt64(plan.TpmLimit), + "keyName": resources_core.FormatString(plan.KeyName), + "teamName": resources_core.FormatString(plan.TeamName), + "variablesKeys": resources_core.FormatString(plan.VariablesKeys), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_secrets", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_secrets", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "secrets", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "secrets", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "secrets", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *LlmAiSecretsResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state LlmAiSecretsModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *LlmAiSecretsResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan LlmAiSecretsModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state LlmAiSecretsModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.LlmAiID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{ + "keyName": resources_core.FormatString(plan.KeyName), + "models": resources_core.FormatString(plan.Models), + "rpmLimit": resources_core.FormatInt64(plan.RpmLimit), + "teamName": resources_core.FormatString(plan.TeamName), + "tpmLimit": resources_core.FormatInt64(plan.TpmLimit), + "variablesKeys": resources_core.FormatString(plan.VariablesKeys), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "secrets", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *LlmAiSecretsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state LlmAiSecretsModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_secrets" == "" { + return + } + + instanceUID := strings.TrimSpace(state.LlmAiID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "keyName": resources_core.FormatString(state.KeyName), + "models": resources_core.FormatString(state.Models), + "rpmLimit": resources_core.FormatInt64(state.RpmLimit), + "teamName": resources_core.FormatString(state.TeamName), + "tpmLimit": resources_core.FormatInt64(state.TpmLimit), + "variablesKeys": resources_core.FormatString(state.VariablesKeys), + } + identityCodes := []string{ + "keyName", + "models", + "rpmLimit", + "teamName", + "tpmLimit", + "variablesKeys", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("secrets", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "keyName": resources_core.FormatString(state.KeyName), + "teamName": resources_core.FormatString(state.TeamName), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_secrets", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *LlmAiSecretsResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/163_llm_ai_team_resource.go b/generated/dev/go/163_llm_ai_team_resource.go new file mode 100644 index 0000000..e411362 --- /dev/null +++ b/generated/dev/go/163_llm_ai_team_resource.go @@ -0,0 +1,365 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: llm_ai +// Subresource: team + +var _ resource.Resource = &LlmAiTeamResource{} + +// LlmAiTeamResource manages a subresource via operations. +type LlmAiTeamResource struct { + client *core.UniversalClient +} + +type LlmAiTeamModel struct { + ID types.String `tfsdk:"id"` + LlmAiID types.String `tfsdk:"llm_ai_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + Models types.String `tfsdk:"models"` + RpmLimit types.Int64 `tfsdk:"rpm_limit"` + TeamName types.String `tfsdk:"team_name"` + TpmLimit types.Int64 `tfsdk:"tpm_limit"` +} + +func NewLlmAiTeamResource() resource.Resource { + return &LlmAiTeamResource{} +} + +func (r *LlmAiTeamResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_llm_ai_team" +} + +func (r *LlmAiTeamResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "llm_ai_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "models": schema.StringAttribute{Required: true}, + "rpm_limit": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(60)}, + "team_name": schema.StringAttribute{Required: true, MarkdownDescription: "Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "tpm_limit": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100000)}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *LlmAiTeamResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan LlmAiTeamModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if plan.Models.IsNull() || plan.Models.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "models is required.") + return + } + if plan.TeamName.IsNull() || plan.TeamName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "team_name is required.") + return + } + + instanceUID := strings.TrimSpace(plan.LlmAiID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "teamName": resources_core.FormatString(plan.TeamName), + } + identityCodes := []string{ + "teamName", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("team", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "team", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "teamName": resources_core.FormatString(plan.TeamName), + "models": resources_core.FormatString(plan.Models), + "rpmLimit": resources_core.FormatInt64(plan.RpmLimit), + "tpmLimit": resources_core.FormatInt64(plan.TpmLimit), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_team", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_team", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "team", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "team", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "team", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *LlmAiTeamResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state LlmAiTeamModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *LlmAiTeamResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan LlmAiTeamModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "modify_team" == "" { + var state LlmAiTeamModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.LlmAiID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{ + "teamName": resources_core.FormatString(plan.TeamName), + "models": resources_core.FormatString(plan.Models), + "rpmLimit": resources_core.FormatInt64(plan.RpmLimit), + "tpmLimit": resources_core.FormatInt64(plan.TpmLimit), + }) + + idParams := map[string]string{ + "teamName": resources_core.FormatString(plan.TeamName), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "modify_team", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "team", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *LlmAiTeamResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state LlmAiTeamModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_team" == "" { + return + } + + instanceUID := strings.TrimSpace(state.LlmAiID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "teamName": resources_core.FormatString(state.TeamName), + } + identityCodes := []string{ + "teamName", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("team", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "teamName": resources_core.FormatString(state.TeamName), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_team", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *LlmAiTeamResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/175_k8s_go_resource.go b/generated/dev/go/175_k8s_go_resource.go new file mode 100644 index 0000000..f6cc30f --- /dev/null +++ b/generated/dev/go/175_k8s_go_resource.go @@ -0,0 +1,422 @@ +package resources_gen + +import ( + "context" + "fmt" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: k8s_go +// Service ID: 175 + +var _ resource.Resource = &K8sGoResource{} +var _ resource.ResourceWithModifyPlan = &K8sGoResource{} +var _ resource.ResourceWithImportState = &K8sGoResource{} + +type K8sGoResource struct { + client *core.UniversalClient +} + +// K8sGoAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration. +type K8sGoAccessConfigurationModel struct { + Domain types.String `tfsdk:"domain" json:"domain"` +} + +// K8sGoAppConfigurationModel — вложенная модель для map-fixed параметра appConfiguration. +type K8sGoAppConfigurationModel struct { + Version types.String `tfsdk:"version" json:"version"` + GitPath types.String `tfsdk:"git_path" json:"gitPath"` + HealthPath types.String `tfsdk:"health_path" json:"healthPath"` +} + +// K8sGoClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type K8sGoClusterConfigurationModel struct { + Cpu types.Int64 `tfsdk:"cpu" json:"cpu"` + Memory types.Int64 `tfsdk:"memory" json:"memory"` + Replicas types.Int64 `tfsdk:"replicas" json:"replicas"` +} + +// K8sGoStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type K8sGoStartupConfigurationModel struct { + ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"` +} + +type K8sGoModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessConfiguration *K8sGoAccessConfigurationModel `tfsdk:"access_configuration"` + AppConfiguration *K8sGoAppConfigurationModel `tfsdk:"app_configuration"` + ClusterConfiguration *K8sGoClusterConfigurationModel `tfsdk:"cluster_configuration"` + JsonEnv types.String `tfsdk:"json_env"` + StartupConfiguration *K8sGoStartupConfigurationModel `tfsdk:"startup_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewK8sGoResource() resource.Resource { + return &K8sGoResource{} +} + +func (r *K8sGoResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_k8s_go" +} + +func (r *K8sGoResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("lucee-utovgtao"), MarkdownDescription: "Если указан FQDN в формате: `myapp.example.com`, предварительно будет проверяться DNS-запись (А-запись **должна смотреть на IP Ingress кластера ресурсной платформы**)

Если указан в формате `myapp`, DNS-запись будет автоматически создана в служебной DNS-зоне"}, + }}, + "app_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("6.0")}, + "git_path": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("https://github.com/xahys/testlucee"), MarkdownDescription: "Путь до репозитория приложения
Обязательно должен быть передан как URL: `https://....`

Должен быть сетевой доступ до URL для скачивания исходников"}, + "health_path": schema.StringAttribute{Optional: true, MarkdownDescription: "Путь до healtcheck для запуска пода в Kubernetes, который отдаст 200 код
Пустое значение означает, что будет проверяться исключительно доступность приложения по порту"}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(200), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"}, + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(256), MarkdownDescription: "Указывается в Megabytes"}, + "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)}, + }}, + "json_env": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("{}")}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"}, + }}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *K8sGoResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *K8sGoModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *K8sGoModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan K8sGoModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 1046: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}), + 1047: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64())}), + 1048: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", config.AccessConfiguration.Domain.ValueString())}), + 1049: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", config.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", config.AppConfiguration.HealthPath.ValueString())}), + 1050: resources_core.FormatString(config.JsonEnv), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 175, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *K8sGoResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data K8sGoModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 175, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 1046: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}), + 1047: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64())}), + 1048: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", data.AccessConfiguration.Domain.ValueString())}), + 1049: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", data.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", data.AppConfiguration.HealthPath.ValueString())}), + 1050: resources_core.FormatString(data.JsonEnv), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 175, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 175, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "jsonEnv", Field: "JsonEnv", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *K8sGoResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state K8sGoModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 175, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "jsonEnv", Field: "JsonEnv", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *K8sGoResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan K8sGoModel + var state K8sGoModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.JsonEnv.IsNull() != state.JsonEnv.IsNull() || plan.JsonEnv.IsUnknown() != state.JsonEnv.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.JsonEnv.IsNull() && !plan.JsonEnv.IsUnknown() && !state.JsonEnv.IsNull() && !state.JsonEnv.IsUnknown() { + if !strings.EqualFold(plan.JsonEnv.ValueString(), state.JsonEnv.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 1051: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64())}), + 1052: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", plan.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", plan.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", plan.AppConfiguration.HealthPath.ValueString())}), + 1053: resources_core.FormatString(plan.JsonEnv), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 175, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "jsonEnv", Field: "JsonEnv", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *K8sGoResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state K8sGoModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *K8sGoResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *K8sGoResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/19_vc_org_resource.go b/generated/dev/go/19_vc_org_resource.go new file mode 100644 index 0000000..c2e768b --- /dev/null +++ b/generated/dev/go/19_vc_org_resource.go @@ -0,0 +1,391 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vc_org +// Service ID: 19 + +var _ resource.Resource = &VcOrgResource{} +var _ resource.ResourceWithModifyPlan = &VcOrgResource{} +var _ resource.ResourceWithImportState = &VcOrgResource{} + +type VcOrgResource struct { + client *core.UniversalClient +} + +type VcOrgModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + OrganizationType types.String `tfsdk:"organization_type"` + ResourceRealm types.String `tfsdk:"resource_realm"` + VIPConfigure types.String `tfsdk:"v_i_p_configure"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewVcOrgResource() resource.Resource { + return &VcOrgResource{} +} + +func (r *VcOrgResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vc_org" +} + +func (r *VcOrgResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "organization_type": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("iaas"), MarkdownDescription: "Доступ к организации предоставляется только при выборе `iaas` Не изменяется после создания."}, + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("sandbox.nubes.ru"), MarkdownDescription: "Облако в котором будет развёрнут тенант Не изменяется после создания."}, + "v_i_p_configure": schema.StringAttribute{Required: true}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VcOrgResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *VcOrgModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *VcOrgModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan VcOrgModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.OrganizationType.IsNull() && !plan.OrganizationType.IsUnknown() && !state.OrganizationType.IsNull() && !state.OrganizationType.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.OrganizationType.ValueString(), state.OrganizationType.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить organization_type", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.VIPConfigure.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "v_i_p_configure is required.") + return + } + if config.VIPConfigure.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 418: resources_core.FormatString(config.ResourceRealm), + 556: resources_core.FormatString(config.OrganizationType), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 19, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *VcOrgResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data VcOrgModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.VIPConfigure.IsNull() || data.VIPConfigure.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "v_i_p_configure is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 19, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 418: resources_core.FormatString(data.ResourceRealm), + 556: resources_core.FormatString(data.OrganizationType), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 19, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 19, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "organizationType", Field: "OrganizationType", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "vIPConfigure", Field: "VIPConfigure", Type: "array-map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcOrgResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VcOrgModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 19, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "organizationType", Field: "OrganizationType", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "vIPConfigure", Field: "VIPConfigure", Type: "array-map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *VcOrgResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VcOrgModel + var state VcOrgModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.VIPConfigure.IsNull() != state.VIPConfigure.IsNull() || plan.VIPConfigure.IsUnknown() != state.VIPConfigure.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.VIPConfigure.IsNull() && !plan.VIPConfigure.IsUnknown() && !state.VIPConfigure.IsNull() && !state.VIPConfigure.IsUnknown() { + if !strings.EqualFold(plan.VIPConfigure.ValueString(), state.VIPConfigure.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 662: resources_core.FormatString(plan.VIPConfigure), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 19, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "organizationType", Field: "OrganizationType", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "vIPConfigure", Field: "VIPConfigure", Type: "array-map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcOrgResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VcOrgModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *VcOrgResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *VcOrgResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/19_vc_org_user_resource.go b/generated/dev/go/19_vc_org_user_resource.go new file mode 100644 index 0000000..b18fcdd --- /dev/null +++ b/generated/dev/go/19_vc_org_user_resource.go @@ -0,0 +1,343 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vc_org +// Subresource: user + +var _ resource.Resource = &VcOrgUserResource{} + +// VcOrgUserResource manages a subresource via operations. +type VcOrgUserResource struct { + client *core.UniversalClient +} + +type VcOrgUserModel struct { + ID types.String `tfsdk:"id"` + VcOrgID types.String `tfsdk:"vc_org_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + Admins types.String `tfsdk:"admins"` +} + +func NewVcOrgUserResource() resource.Resource { + return &VcOrgUserResource{} +} + +func (r *VcOrgUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vc_org_user" +} + +func (r *VcOrgUserResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "vc_org_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "admins": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VcOrgUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan VcOrgUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.VcOrgID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "admins": resources_core.FormatString(plan.Admins), + } + identityCodes := []string{ + "admins", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "admins": resources_core.FormatString(plan.Admins), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *VcOrgUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VcOrgUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcOrgUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VcOrgUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state VcOrgUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.VcOrgID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{ + "admins": resources_core.FormatString(plan.Admins), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *VcOrgUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VcOrgUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_user" == "" { + return + } + + instanceUID := strings.TrimSpace(state.VcOrgID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "admins": resources_core.FormatString(state.Admins), + } + identityCodes := []string{ + "admins", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "admins": resources_core.FormatString(state.Admins), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_user", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *VcOrgUserResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/1_dummy_resource.go b/generated/dev/go/1_dummy_resource.go new file mode 100644 index 0000000..e2070da --- /dev/null +++ b/generated/dev/go/1_dummy_resource.go @@ -0,0 +1,582 @@ +package resources_gen + +import ( + "context" + "fmt" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: dummy +// Service ID: 1 + +var _ resource.Resource = &DummyResource{} +var _ resource.ResourceWithModifyPlan = &DummyResource{} +var _ resource.ResourceWithImportState = &DummyResource{} + +type DummyResource struct { + client *core.UniversalClient +} + +// DummyMapExampleModel — вложенная модель для map-fixed параметра mapExample. +type DummyMapExampleModel struct { + Subparam1 types.String `tfsdk:"subparam1" json:"subparam1"` + Secret types.String `tfsdk:"secret" json:"secret"` + Subparam2 types.String `tfsdk:"subparam2" json:"subparam2"` +} + +// DummyMapFixedModel — вложенная модель для map-fixed параметра mapFixed. +type DummyMapFixedModel struct { + Bol1 types.Bool `tfsdk:"bol1" json:"bol1"` + MinStr1 types.String `tfsdk:"min_str1" json:"minStr1"` + Param1 types.Int64 `tfsdk:"param1" json:"param1"` + Param2 types.String `tfsdk:"param2" json:"param2"` + Param3 types.Int64 `tfsdk:"param3" json:"param3"` +} + +type DummyModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + // --- Redeploy support (ARCHITECTURE.md) --- + // git_revision: при изменении вызывает redeploy вместо modify. + // Опциональное поле — если не задано, modify работает как обычно. + GitRevision types.String `tfsdk:"git_revision"` + Arr types.String `tfsdk:"arr"` + ArrayMapFixedExample types.String `tfsdk:"array_map_fixed_example"` + Bodymessage types.String `tfsdk:"bodymessage"` + DurationMs types.Int64 `tfsdk:"duration_ms"` + FailAtStart types.Bool `tfsdk:"fail_at_start"` + FailInProgress types.Bool `tfsdk:"fail_in_progress"` + JsonExample types.String `tfsdk:"json_example"` + MapExample *DummyMapExampleModel `tfsdk:"map_example"` + MapFixed *DummyMapFixedModel `tfsdk:"map_fixed"` + NestedRefExample types.String `tfsdk:"nested_ref_example"` + ResourceRealm types.String `tfsdk:"resource_realm"` + WhereFail types.Int64 `tfsdk:"where_fail"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewDummyResource() resource.Resource { + return &DummyResource{} +} + +func (r *DummyResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_dummy" +} + +func (r *DummyResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "arr": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания."}, + "array_map_fixed_example": schema.StringAttribute{Optional: true, MarkdownDescription: "Array of map-fixed Не изменяется после создания."}, + "bodymessage": schema.StringAttribute{Optional: true, MarkdownDescription: "Дата, уходящая в Vault"}, + "duration_ms": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(0), MarkdownDescription: "перед завершением подождать (sleep) столько миллисекунд"}, + "fail_at_start": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "если true, завершить джоб, ничего не записывая в cmdb"}, + "fail_in_progress": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "если true, корректно начать выполнение джоба, но в процессе завершить аварийно, ничего не записывая в cmdb"}, + "json_example": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "Для тестирования интерфейса", PlanModifiers: []planmodifier.String{resources_core.JsonNormalize()}}, + "map_example": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "subparam1": schema.StringAttribute{Optional: true, MarkdownDescription: "это для администратора"}, + "secret": schema.StringAttribute{Optional: true, MarkdownDescription: "это секрет"}, + "subparam2": schema.StringAttribute{Optional: true}, + }}, + "map_fixed": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "bol1": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Булевский, обязательный"}, + "min_str1": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("abracadabra"), MarkdownDescription: "не меньше 10 символов"}, + "param1": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Integer, secret"}, + "param2": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("1"), MarkdownDescription: "Дескрипшен"}, + "param3": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "целое положительное"}, + }}, + "nested_ref_example": schema.StringAttribute{Optional: true, MarkdownDescription: "Не изменяется после создания."}, + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("dummy"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + "where_fail": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "принимает 1,2,3. Работает с failInProgress. 1 - упасть в prepare. 2 - упасть внутри блока Data fill. 3 - упасть после vault write"}, + // --- Redeploy support (ARCHITECTURE.md) --- + // При изменении git_revision вызывается redeploy вместо modify. + // Если не задано — modify работает без изменений. + "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *DummyResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *DummyModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *DummyModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan DummyModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.Arr.IsNull() && !plan.Arr.IsUnknown() && !state.Arr.IsNull() && !state.Arr.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Arr.ValueString(), state.Arr.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить arr", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ArrayMapFixedExample.IsNull() && !plan.ArrayMapFixedExample.IsUnknown() && !state.ArrayMapFixedExample.IsNull() && !state.ArrayMapFixedExample.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ArrayMapFixedExample.ValueString(), state.ArrayMapFixedExample.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить array_map_fixed_example", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.NestedRefExample.IsNull() && !plan.NestedRefExample.IsUnknown() && !state.NestedRefExample.IsNull() && !state.NestedRefExample.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.NestedRefExample.ValueString(), state.NestedRefExample.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить nested_ref_example", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.Arr.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "arr is required.") + return + } + if config.Arr.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 198: resources_core.FormatInt64(config.DurationMs), + 199: resources_core.FormatBool(config.FailAtStart), + 200: resources_core.FormatBool(config.FailInProgress), + 201: resources_core.FormatInt64(config.WhereFail), + 242: resources_core.FormatString(config.ResourceRealm), + 286: resources_core.FormatString(config.Bodymessage), + 321: resources_core.BuildJSON(map[string]string{"subparam1": fmt.Sprintf("\"%s\"", config.MapExample.Subparam1.ValueString()), "secret": fmt.Sprintf("\"%s\"", config.MapExample.Secret.ValueString()), "subparam2": fmt.Sprintf("\"%s\"", config.MapExample.Subparam2.ValueString())}), + 322: resources_core.FormatString(config.JsonExample), + 396: resources_core.FormatString(config.NestedRefExample), + 647: resources_core.BuildJSON(map[string]string{"bol1": fmt.Sprintf("%v", config.MapFixed.Bol1.ValueBool()), "minStr1": fmt.Sprintf("\"%s\"", config.MapFixed.MinStr1.ValueString()), "param1": fmt.Sprintf("%d", config.MapFixed.Param1.ValueInt64()), "param2": fmt.Sprintf("\"%s\"", config.MapFixed.Param2.ValueString()), "param3": fmt.Sprintf("%d", config.MapFixed.Param3.ValueInt64())}), + 654: resources_core.FormatString(config.ArrayMapFixedExample), + 863: resources_core.FormatString(config.Arr), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 1, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *DummyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data DummyModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.Arr.IsNull() || data.Arr.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "arr is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 1, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 198: resources_core.FormatInt64(data.DurationMs), + 199: resources_core.FormatBool(data.FailAtStart), + 200: resources_core.FormatBool(data.FailInProgress), + 201: resources_core.FormatInt64(data.WhereFail), + 242: resources_core.FormatString(data.ResourceRealm), + 286: resources_core.FormatString(data.Bodymessage), + 321: resources_core.BuildJSON(map[string]string{"subparam1": fmt.Sprintf("\"%s\"", data.MapExample.Subparam1.ValueString()), "secret": fmt.Sprintf("\"%s\"", data.MapExample.Secret.ValueString()), "subparam2": fmt.Sprintf("\"%s\"", data.MapExample.Subparam2.ValueString())}), + 322: resources_core.FormatString(data.JsonExample), + 396: resources_core.FormatString(data.NestedRefExample), + 647: resources_core.BuildJSON(map[string]string{"bol1": fmt.Sprintf("%v", data.MapFixed.Bol1.ValueBool()), "minStr1": fmt.Sprintf("\"%s\"", data.MapFixed.MinStr1.ValueString()), "param1": fmt.Sprintf("%d", data.MapFixed.Param1.ValueInt64()), "param2": fmt.Sprintf("\"%s\"", data.MapFixed.Param2.ValueString()), "param3": fmt.Sprintf("%d", data.MapFixed.Param3.ValueInt64())}), + 654: resources_core.FormatString(data.ArrayMapFixedExample), + 863: resources_core.FormatString(data.Arr), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 1, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 1, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "arr", Field: "Arr", Type: "string"}, + {Code: "arrayMapFixedExample", Field: "ArrayMapFixedExample", Type: "array-map-fixed"}, + {Code: "bodymessage", Field: "Bodymessage", Type: "string"}, + {Code: "durationMs", Field: "DurationMs", Type: "int64"}, + {Code: "failAtStart", Field: "FailAtStart", Type: "bool"}, + {Code: "failInProgress", Field: "FailInProgress", Type: "bool"}, + {Code: "jsonExample", Field: "JsonExample", Type: "string"}, + {Code: "mapExample", Field: "MapExample", Type: "string"}, + {Code: "mapFixed", Field: "MapFixed", Type: "map-fixed"}, + {Code: "nestedRefExample", Field: "NestedRefExample", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "whereFail", Field: "WhereFail", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *DummyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state DummyModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 1, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "arr", Field: "Arr", Type: "string"}, + {Code: "arrayMapFixedExample", Field: "ArrayMapFixedExample", Type: "array-map-fixed"}, + {Code: "bodymessage", Field: "Bodymessage", Type: "string"}, + {Code: "durationMs", Field: "DurationMs", Type: "int64"}, + {Code: "failAtStart", Field: "FailAtStart", Type: "bool"}, + {Code: "failInProgress", Field: "FailInProgress", Type: "bool"}, + {Code: "jsonExample", Field: "JsonExample", Type: "string"}, + {Code: "mapExample", Field: "MapExample", Type: "string"}, + {Code: "mapFixed", Field: "MapFixed", Type: "map-fixed"}, + {Code: "nestedRefExample", Field: "NestedRefExample", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "whereFail", Field: "WhereFail", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *DummyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan DummyModel + var state DummyModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.DurationMs.IsNull() != state.DurationMs.IsNull() || plan.DurationMs.IsUnknown() != state.DurationMs.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.DurationMs.IsNull() && !plan.DurationMs.IsUnknown() && !state.DurationMs.IsNull() && !state.DurationMs.IsUnknown() { + if plan.DurationMs.ValueInt64() != state.DurationMs.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.FailAtStart.IsNull() != state.FailAtStart.IsNull() || plan.FailAtStart.IsUnknown() != state.FailAtStart.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.FailAtStart.IsNull() && !plan.FailAtStart.IsUnknown() && !state.FailAtStart.IsNull() && !state.FailAtStart.IsUnknown() { + if plan.FailAtStart.ValueBool() != state.FailAtStart.ValueBool() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.FailInProgress.IsNull() != state.FailInProgress.IsNull() || plan.FailInProgress.IsUnknown() != state.FailInProgress.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.FailInProgress.IsNull() && !plan.FailInProgress.IsUnknown() && !state.FailInProgress.IsNull() && !state.FailInProgress.IsUnknown() { + if plan.FailInProgress.ValueBool() != state.FailInProgress.ValueBool() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.WhereFail.IsNull() != state.WhereFail.IsNull() || plan.WhereFail.IsUnknown() != state.WhereFail.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.WhereFail.IsNull() && !plan.WhereFail.IsUnknown() && !state.WhereFail.IsNull() && !state.WhereFail.IsUnknown() { + if plan.WhereFail.ValueInt64() != state.WhereFail.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.Bodymessage.IsNull() != state.Bodymessage.IsNull() || plan.Bodymessage.IsUnknown() != state.Bodymessage.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.Bodymessage.IsNull() && !plan.Bodymessage.IsUnknown() && !state.Bodymessage.IsNull() && !state.Bodymessage.IsUnknown() { + if !strings.EqualFold(plan.Bodymessage.ValueString(), state.Bodymessage.ValueString()) { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.JsonExample.IsNull() != state.JsonExample.IsNull() || plan.JsonExample.IsUnknown() != state.JsonExample.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.JsonExample.IsNull() && !plan.JsonExample.IsUnknown() && !state.JsonExample.IsNull() && !state.JsonExample.IsUnknown() { + if !strings.EqualFold(plan.JsonExample.ValueString(), state.JsonExample.ValueString()) { + hasServiceParamChanges = true + } + } + } + // --- Redeploy support (ARCHITECTURE.md) --- + // Проверяем изменился ли git_revision. + // Если да — вызываем redeploy вместо modify. + // git_revision = "" (не задано) → обычный modify. + redeployRequested := false + if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() { + if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() { + redeployRequested = true + } + } + + if !hasServiceParamChanges && !redeployRequested { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + // --- Redeploy support (ARCHITECTURE.md) --- + if redeployRequested { + // Вызываем redeploy — пустой params (операция не требует CFS-параметров) + if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + } else { + params := map[int]string{ + 287: resources_core.FormatInt64(plan.DurationMs), + 288: resources_core.FormatBool(plan.FailAtStart), + 289: resources_core.FormatBool(plan.FailInProgress), + 290: resources_core.FormatInt64(plan.WhereFail), + 291: resources_core.FormatString(plan.Bodymessage), + 373: resources_core.BuildJSON(map[string]string{"secret": fmt.Sprintf("\"%s\"", plan.MapExample.Secret.ValueString())}), + 374: resources_core.FormatString(plan.JsonExample), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 1, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "arr", Field: "Arr", Type: "string"}, + {Code: "arrayMapFixedExample", Field: "ArrayMapFixedExample", Type: "array-map-fixed"}, + {Code: "bodymessage", Field: "Bodymessage", Type: "string"}, + {Code: "durationMs", Field: "DurationMs", Type: "int64"}, + {Code: "failAtStart", Field: "FailAtStart", Type: "bool"}, + {Code: "failInProgress", Field: "FailInProgress", Type: "bool"}, + {Code: "jsonExample", Field: "JsonExample", Type: "string"}, + {Code: "mapExample", Field: "MapExample", Type: "string"}, + {Code: "mapFixed", Field: "MapFixed", Type: "map-fixed"}, + {Code: "nestedRefExample", Field: "NestedRefExample", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "whereFail", Field: "WhereFail", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *DummyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state DummyModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *DummyResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *DummyResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/20_vc_org_saas_resource.go b/generated/dev/go/20_vc_org_saas_resource.go new file mode 100644 index 0000000..83cdd6e --- /dev/null +++ b/generated/dev/go/20_vc_org_saas_resource.go @@ -0,0 +1,348 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vc_org_saas +// Service ID: 20 + +var _ resource.Resource = &VcOrgSaasResource{} +var _ resource.ResourceWithModifyPlan = &VcOrgSaasResource{} +var _ resource.ResourceWithImportState = &VcOrgSaasResource{} + +type VcOrgSaasResource struct { + client *core.UniversalClient +} + +type VcOrgSaasModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + ResourceRealm types.String `tfsdk:"resource_realm"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewVcOrgSaasResource() resource.Resource { + return &VcOrgSaasResource{} +} + +func (r *VcOrgSaasResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vc_org_saas" +} + +func (r *VcOrgSaasResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("sandbox.nubes.ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VcOrgSaasResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *VcOrgSaasModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *VcOrgSaasModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan VcOrgSaasModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 185: resources_core.FormatString(config.ResourceRealm), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 20, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *VcOrgSaasResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data VcOrgSaasModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 20, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 185: resources_core.FormatString(data.ResourceRealm), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 20, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 20, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcOrgSaasResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VcOrgSaasModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 20, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *VcOrgSaasResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VcOrgSaasModel + var state VcOrgSaasModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 20, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcOrgSaasResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VcOrgSaasModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *VcOrgSaasResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *VcOrgSaasResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/21_vc_vdc_resource.go b/generated/dev/go/21_vc_vdc_resource.go new file mode 100644 index 0000000..21d0d3d --- /dev/null +++ b/generated/dev/go/21_vc_vdc_resource.go @@ -0,0 +1,573 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vc_vdc +// Service ID: 21 + +var _ resource.Resource = &VcVdcResource{} +var _ resource.ResourceWithModifyPlan = &VcVdcResource{} +var _ resource.ResourceWithImportState = &VcVdcResource{} + +type VcVdcResource struct { + client *core.UniversalClient +} + +type VcVdcModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + CpuAllocated types.Int64 `tfsdk:"cpu_allocated"` + CpuGuaranteed types.Int64 `tfsdk:"cpu_guaranteed"` + MemAllocated types.Int64 `tfsdk:"mem_allocated"` + NetworkProvider types.String `tfsdk:"network_provider"` + OrganizationUid types.String `tfsdk:"organization_uid"` + ProviderVdc types.String `tfsdk:"provider_vdc"` + StorageConfig types.String `tfsdk:"storage_config"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewVcVdcResource() resource.Resource { + return &VcVdcResource{} +} + +func (r *VcVdcResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vc_vdc" +} + +func (r *VcVdcResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "cpu_allocated": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(80), MarkdownDescription: "шт"}, + "cpu_guaranteed": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(20), MarkdownDescription: "% резервирования Не изменяется после создания."}, + "mem_allocated": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(200), MarkdownDescription: "gb"}, + "network_provider": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания."}, + "organization_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "Для создания VDC предварительно необходимо создать организацию через услугу `Организация в Cloud Director` Не изменяется после создания."}, + "provider_vdc": schema.StringAttribute{Required: true, MarkdownDescription: "Приписка `2.8` - Скорость vCPU - 2800MHz
И так далее...
Не изменяется после создания."}, + "storage_config": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Первая политика в списке указывается как "По умолчанию""}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VcVdcResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *VcVdcModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *VcVdcModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan VcVdcModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan. + // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов. + // Resolve (displayName → UUID или uppercase → lowercase) нужен только для + // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка + // "Provider produced invalid plan: planned value does not match config value". + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.CpuGuaranteed.IsNull() && !plan.CpuGuaranteed.IsUnknown() && !state.CpuGuaranteed.IsNull() && !state.CpuGuaranteed.IsUnknown() { + if plan.CpuGuaranteed.ValueInt64() != state.CpuGuaranteed.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить cpu_guaranteed", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.NetworkProvider.IsNull() && !plan.NetworkProvider.IsUnknown() && !state.NetworkProvider.IsNull() && !state.NetworkProvider.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.NetworkProvider.ValueString(), state.NetworkProvider.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить network_provider", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.OrganizationUid.IsNull() && !plan.OrganizationUid.IsUnknown() && !state.OrganizationUid.IsNull() && !state.OrganizationUid.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.OrganizationUid.ValueString(), state.OrganizationUid.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить organization_uid", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ProviderVdc.IsNull() && !plan.ProviderVdc.IsUnknown() && !state.ProviderVdc.IsNull() && !state.ProviderVdc.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ProviderVdc.ValueString(), state.ProviderVdc.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить provider_vdc", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.NetworkProvider.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "network_provider is required.") + return + } + if config.NetworkProvider.IsUnknown() { + return + } + if config.OrganizationUid.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "organization_uid is required.") + return + } + if config.OrganizationUid.IsUnknown() { + return + } + if config.ProviderVdc.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "provider_vdc is required.") + return + } + if config.ProviderVdc.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + if !config.OrganizationUid.IsNull() && !config.OrganizationUid.IsUnknown() { + resolvedOrganizationUid, err := r.client.ResolveRefSvcParamValue(ctx, 19, config.OrganizationUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve organization_uid", err.Error()) + } else if resolvedOrganizationUid != "" && resolvedOrganizationUid != config.OrganizationUid.ValueString() { + config.OrganizationUid = types.StringValue(resolvedOrganizationUid) + } + } + params := map[int]string{ + 30: resources_core.FormatString(config.OrganizationUid), + 335: resources_core.FormatString(config.ProviderVdc), + 361: resources_core.FormatString(config.StorageConfig), + 397: resources_core.FormatInt64(config.CpuGuaranteed), + 557: resources_core.FormatInt64(config.CpuAllocated), + 558: resources_core.FormatInt64(config.MemAllocated), + 746: resources_core.FormatString(config.NetworkProvider), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 21, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *VcVdcResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data VcVdcModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.NetworkProvider.IsNull() || data.NetworkProvider.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "network_provider is required.") + return + } + if data.OrganizationUid.IsNull() || data.OrganizationUid.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "organization_uid is required.") + return + } + if data.ProviderVdc.IsNull() || data.ProviderVdc.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "provider_vdc is required.") + return + } + // ══════════════════════════════════════════════════════════════════════════ + // ПРАВИЛО TERRAFORM (официальная документация): + // «If an attribute value is configured, it is NEVER valid to change that + // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что + // написал пользователь). Менять plan запрещено на уровне фреймворка. + // + // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре. + // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный). + // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит: + // «Provider produced inconsistent result after apply». + // + // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот. + // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как + // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API). + // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный + // регистр в state через strings.EqualFold (сравниваем без учёта регистра). + // ══════════════════════════════════════════════════════════════════════════ + originalOrganizationUid := data.OrganizationUid + if !data.OrganizationUid.IsNull() && !data.OrganizationUid.IsUnknown() { + resolvedOrganizationUid, err := r.client.ResolveRefSvcParamValue(ctx, 19, data.OrganizationUid.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if resolvedOrganizationUid != "" && resolvedOrganizationUid != data.OrganizationUid.ValueString() { + data.OrganizationUid = types.StringValue(resolvedOrganizationUid) + } + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 21, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 30: resources_core.FormatString(data.OrganizationUid), + 335: resources_core.FormatString(data.ProviderVdc), + 361: resources_core.FormatString(data.StorageConfig), + 397: resources_core.FormatInt64(data.CpuGuaranteed), + 557: resources_core.FormatInt64(data.CpuAllocated), + 558: resources_core.FormatInt64(data.MemAllocated), + 746: resources_core.FormatString(data.NetworkProvider), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 21, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 21, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "cpuAllocated", Field: "CpuAllocated", Type: "int64"}, + {Code: "cpuGuaranteed", Field: "CpuGuaranteed", Type: "int64"}, + {Code: "memAllocated", Field: "MemAllocated", Type: "int64"}, + {Code: "networkProvider", Field: "NetworkProvider", Type: "string"}, + {Code: "organizationUid", Field: "OrganizationUid", Type: "string"}, + {Code: "providerVdc", Field: "ProviderVdc", Type: "string"}, + {Code: "storageConfig", Field: "StorageConfig", Type: "array-map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.OrganizationUid.IsNull() && !state.OrganizationUid.IsUnknown() { + resolvedOrganizationUid, err := r.client.ResolveRefSvcParamValue(ctx, 19, state.OrganizationUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve organization_uid", err.Error()) + } else if resolvedOrganizationUid != "" && resolvedOrganizationUid != state.OrganizationUid.ValueString() { + state.OrganizationUid = types.StringValue(resolvedOrganizationUid) + } + } + // Restore user-provided casing в state (Create). + // EqualFold = «совпадают ли значения без учёта регистра?» + // Если да — значит API вернул «туже» строку, только в другом регистре. + // Заменяем state на original (то что было в plan/config пользователя). + // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint. + if !originalOrganizationUid.IsNull() && !originalOrganizationUid.IsUnknown() && !state.OrganizationUid.IsNull() && !state.OrganizationUid.IsUnknown() { + if strings.EqualFold(state.OrganizationUid.ValueString(), originalOrganizationUid.ValueString()) { + state.OrganizationUid = originalOrganizationUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcVdcResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VcVdcModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 21, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "cpuAllocated", Field: "CpuAllocated", Type: "int64"}, + {Code: "cpuGuaranteed", Field: "CpuGuaranteed", Type: "int64"}, + {Code: "memAllocated", Field: "MemAllocated", Type: "int64"}, + {Code: "networkProvider", Field: "NetworkProvider", Type: "string"}, + {Code: "organizationUid", Field: "OrganizationUid", Type: "string"}, + {Code: "providerVdc", Field: "ProviderVdc", Type: "string"}, + {Code: "storageConfig", Field: "StorageConfig", Type: "array-map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !newState.OrganizationUid.IsNull() && !newState.OrganizationUid.IsUnknown() { + resolvedOrganizationUid, err := r.client.ResolveRefSvcParamValue(ctx, 19, newState.OrganizationUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve organization_uid", err.Error()) + } else if resolvedOrganizationUid != "" && resolvedOrganizationUid != newState.OrganizationUid.ValueString() { + newState.OrganizationUid = types.StringValue(resolvedOrganizationUid) + } + } + // Restore user-provided casing в state (Read). + // При чтении у нас нет plan — но предыдущий state уже хранит значение + // в регистре пользователя (после первого Create оно было восстановлено). + // Берём prior state (переменная state) как эталон регистра. + // Если API вернул то же UUID только строчными буквами — восстанавливаем. + if !state.OrganizationUid.IsNull() && !state.OrganizationUid.IsUnknown() && !newState.OrganizationUid.IsNull() && !newState.OrganizationUid.IsUnknown() { + if strings.EqualFold(newState.OrganizationUid.ValueString(), state.OrganizationUid.ValueString()) { + newState.OrganizationUid = state.OrganizationUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *VcVdcResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VcVdcModel + var state VcVdcModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.CpuAllocated.IsNull() != state.CpuAllocated.IsNull() || plan.CpuAllocated.IsUnknown() != state.CpuAllocated.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.CpuAllocated.IsNull() && !plan.CpuAllocated.IsUnknown() && !state.CpuAllocated.IsNull() && !state.CpuAllocated.IsUnknown() { + if plan.CpuAllocated.ValueInt64() != state.CpuAllocated.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.MemAllocated.IsNull() != state.MemAllocated.IsNull() || plan.MemAllocated.IsUnknown() != state.MemAllocated.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.MemAllocated.IsNull() && !plan.MemAllocated.IsUnknown() && !state.MemAllocated.IsNull() && !state.MemAllocated.IsUnknown() { + if plan.MemAllocated.ValueInt64() != state.MemAllocated.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.StorageConfig.IsNull() != state.StorageConfig.IsNull() || plan.StorageConfig.IsUnknown() != state.StorageConfig.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.StorageConfig.IsNull() && !plan.StorageConfig.IsUnknown() && !state.StorageConfig.IsNull() && !state.StorageConfig.IsUnknown() { + if !strings.EqualFold(plan.StorageConfig.ValueString(), state.StorageConfig.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 560: resources_core.FormatInt64(plan.CpuAllocated), + 561: resources_core.FormatInt64(plan.MemAllocated), + 562: resources_core.FormatString(plan.StorageConfig), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 21, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "cpuAllocated", Field: "CpuAllocated", Type: "int64"}, + {Code: "cpuGuaranteed", Field: "CpuGuaranteed", Type: "int64"}, + {Code: "memAllocated", Field: "MemAllocated", Type: "int64"}, + {Code: "networkProvider", Field: "NetworkProvider", Type: "string"}, + {Code: "organizationUid", Field: "OrganizationUid", Type: "string"}, + {Code: "providerVdc", Field: "ProviderVdc", Type: "string"}, + {Code: "storageConfig", Field: "StorageConfig", Type: "array-map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.OrganizationUid.IsNull() && !state.OrganizationUid.IsUnknown() { + resolvedOrganizationUid, err := r.client.ResolveRefSvcParamValue(ctx, 19, state.OrganizationUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve organization_uid", err.Error()) + } else if resolvedOrganizationUid != "" && resolvedOrganizationUid != state.OrganizationUid.ValueString() { + state.OrganizationUid = types.StringValue(resolvedOrganizationUid) + } + } + // Restore user-provided casing в state (Update). + // После API-вызова modify state содержит значения в lower-case от API. + // Plan == config == то что написал пользователь (регистр неизменён). + // EqualFold: если UUID совпадает без учёта регистра — берём из plan. + if !plan.OrganizationUid.IsNull() && !plan.OrganizationUid.IsUnknown() && !state.OrganizationUid.IsNull() && !state.OrganizationUid.IsUnknown() { + if strings.EqualFold(state.OrganizationUid.ValueString(), plan.OrganizationUid.ValueString()) { + state.OrganizationUid = plan.OrganizationUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcVdcResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VcVdcModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *VcVdcResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *VcVdcResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/22_vc_nsxt_resource.go b/generated/dev/go/22_vc_nsxt_resource.go new file mode 100644 index 0000000..58189cc --- /dev/null +++ b/generated/dev/go/22_vc_nsxt_resource.go @@ -0,0 +1,614 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vc_nsxt +// Service ID: 22 + +var _ resource.Resource = &VcNsxtResource{} +var _ resource.ResourceWithModifyPlan = &VcNsxtResource{} +var _ resource.ResourceWithImportState = &VcNsxtResource{} + +type VcNsxtResource struct { + client *core.UniversalClient +} + +type VcNsxtModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + IpSpaceName types.String `tfsdk:"ip_space_name"` + NeedEnableAVI types.Bool `tfsdk:"need_enable_a_v_i"` + QosProfile types.String `tfsdk:"qos_profile"` + VdcGroupUid types.String `tfsdk:"vdc_group_uid"` + VdcType types.String `tfsdk:"vdc_type"` + VdcUid types.String `tfsdk:"vdc_uid"` + VirtualServicesCount types.Int64 `tfsdk:"virtual_services_count"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewVcNsxtResource() resource.Resource { + return &VcNsxtResource{} +} + +func (r *VcNsxtResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vc_nsxt" +} + +func (r *VcNsxtResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "ip_space_name": schema.StringAttribute{Optional: true, MarkdownDescription: "Имя ip Space для внешнего IP"}, + "need_enable_a_v_i": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включение Load Balancer"}, + "qos_profile": schema.StringAttribute{Optional: true, MarkdownDescription: "Параметр пока не работает
Должен возвращать поле из ресурсной платформы типа vc из .vcd.hardware.gatewayQoSProfiles

Как по идее должен работать.
Нужно заполнить или CFS vdcUid, или vdcGroupUid
Идеально конечно проверять `if (params.vdcType == "vdc" && params.vdcUid != "")` или `if (params.vdcType == "vdcGroup" && params.vdcGroupUid != "")`

Далее надо пойти по стейту vdc -> org -> resPlatform, взять gatewayQoSProfiles
Или пойти по стейту vdcgroup -> vdc -> org -> resPlatform, взять gatewayQoSProfiles

Регулярку могу написать (Виталя)"}, + "vdc_group_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UUID Услуги `Группа виртуальных датацентров (groupvDC)` Не изменяется после создания."}, + "vdc_type": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("vdc"), MarkdownDescription: "Тип родительской услуги Не изменяется после создания."}, + "vdc_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UUID Услуги `Виртуальный датацентр (vDC)` Не изменяется после создания."}, + "virtual_services_count": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Кол-во виртуальных сервисов, которые **резервируются** на AVI"}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VcNsxtResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *VcNsxtModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *VcNsxtModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan VcNsxtModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan. + // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов. + // Resolve (displayName → UUID или uppercase → lowercase) нужен только для + // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка + // "Provider produced invalid plan: planned value does not match config value". + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.VdcGroupUid.IsNull() && !plan.VdcGroupUid.IsUnknown() && !state.VdcGroupUid.IsNull() && !state.VdcGroupUid.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VdcGroupUid.ValueString(), state.VdcGroupUid.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить vdc_group_uid", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.VdcType.IsNull() && !plan.VdcType.IsUnknown() && !state.VdcType.IsNull() && !state.VdcType.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VdcType.ValueString(), state.VdcType.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить vdc_type", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.VdcUid.IsNull() && !plan.VdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VdcUid.ValueString(), state.VdcUid.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить vdc_uid", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + if !config.VdcGroupUid.IsNull() && !config.VdcGroupUid.IsUnknown() { + resolvedVdcGroupUid, err := r.client.ResolveRefSvcParamValue(ctx, 29, config.VdcGroupUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_group_uid", err.Error()) + } else if resolvedVdcGroupUid != "" && resolvedVdcGroupUid != config.VdcGroupUid.ValueString() { + config.VdcGroupUid = types.StringValue(resolvedVdcGroupUid) + } + } + if !config.VdcUid.IsNull() && !config.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, config.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error()) + } else if resolvedVdcUid != "" && resolvedVdcUid != config.VdcUid.ValueString() { + config.VdcUid = types.StringValue(resolvedVdcUid) + } + } + params := map[int]string{ + 8: resources_core.FormatString(config.VdcUid), + 340: resources_core.FormatBool(config.NeedEnableAVI), + 341: resources_core.FormatInt64(config.VirtualServicesCount), + 621: resources_core.FormatString(config.VdcType), + 622: resources_core.FormatString(config.VdcGroupUid), + 825: resources_core.FormatString(config.QosProfile), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 22, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *VcNsxtResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data VcNsxtModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + // ══════════════════════════════════════════════════════════════════════════ + // ПРАВИЛО TERRAFORM (официальная документация): + // «If an attribute value is configured, it is NEVER valid to change that + // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что + // написал пользователь). Менять plan запрещено на уровне фреймворка. + // + // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре. + // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный). + // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит: + // «Provider produced inconsistent result after apply». + // + // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот. + // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как + // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API). + // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный + // регистр в state через strings.EqualFold (сравниваем без учёта регистра). + // ══════════════════════════════════════════════════════════════════════════ + originalVdcGroupUid := data.VdcGroupUid + // ══════════════════════════════════════════════════════════════════════════ + // ПРАВИЛО TERRAFORM (официальная документация): + // «If an attribute value is configured, it is NEVER valid to change that + // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что + // написал пользователь). Менять plan запрещено на уровне фреймворка. + // + // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре. + // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный). + // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит: + // «Provider produced inconsistent result after apply». + // + // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот. + // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как + // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API). + // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный + // регистр в state через strings.EqualFold (сравниваем без учёта регистра). + // ══════════════════════════════════════════════════════════════════════════ + originalVdcUid := data.VdcUid + if !data.VdcGroupUid.IsNull() && !data.VdcGroupUid.IsUnknown() { + resolvedVdcGroupUid, err := r.client.ResolveRefSvcParamValue(ctx, 29, data.VdcGroupUid.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if resolvedVdcGroupUid != "" && resolvedVdcGroupUid != data.VdcGroupUid.ValueString() { + data.VdcGroupUid = types.StringValue(resolvedVdcGroupUid) + } + } + if !data.VdcUid.IsNull() && !data.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, data.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if resolvedVdcUid != "" && resolvedVdcUid != data.VdcUid.ValueString() { + data.VdcUid = types.StringValue(resolvedVdcUid) + } + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 22, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 8: resources_core.FormatString(data.VdcUid), + 340: resources_core.FormatBool(data.NeedEnableAVI), + 341: resources_core.FormatInt64(data.VirtualServicesCount), + 621: resources_core.FormatString(data.VdcType), + 622: resources_core.FormatString(data.VdcGroupUid), + 825: resources_core.FormatString(data.QosProfile), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 22, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 22, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "needEnableAVI", Field: "NeedEnableAVI", Type: "bool"}, + {Code: "qosProfile", Field: "QosProfile", Type: "string"}, + {Code: "vdcGroupUid", Field: "VdcGroupUid", Type: "string"}, + {Code: "vdcType", Field: "VdcType", Type: "string"}, + {Code: "vdcUid", Field: "VdcUid", Type: "string"}, + {Code: "virtualServicesCount", Field: "VirtualServicesCount", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.VdcGroupUid.IsNull() && !state.VdcGroupUid.IsUnknown() { + resolvedVdcGroupUid, err := r.client.ResolveRefSvcParamValue(ctx, 29, state.VdcGroupUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_group_uid", err.Error()) + } else if resolvedVdcGroupUid != "" && resolvedVdcGroupUid != state.VdcGroupUid.ValueString() { + state.VdcGroupUid = types.StringValue(resolvedVdcGroupUid) + } + } + if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, state.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error()) + } else if resolvedVdcUid != "" && resolvedVdcUid != state.VdcUid.ValueString() { + state.VdcUid = types.StringValue(resolvedVdcUid) + } + } + // Restore user-provided casing в state (Create). + // EqualFold = «совпадают ли значения без учёта регистра?» + // Если да — значит API вернул «туже» строку, только в другом регистре. + // Заменяем state на original (то что было в plan/config пользователя). + // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint. + if !originalVdcGroupUid.IsNull() && !originalVdcGroupUid.IsUnknown() && !state.VdcGroupUid.IsNull() && !state.VdcGroupUid.IsUnknown() { + if strings.EqualFold(state.VdcGroupUid.ValueString(), originalVdcGroupUid.ValueString()) { + state.VdcGroupUid = originalVdcGroupUid + } + } + if !originalVdcUid.IsNull() && !originalVdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + if strings.EqualFold(state.VdcUid.ValueString(), originalVdcUid.ValueString()) { + state.VdcUid = originalVdcUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcNsxtResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VcNsxtModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 22, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "needEnableAVI", Field: "NeedEnableAVI", Type: "bool"}, + {Code: "qosProfile", Field: "QosProfile", Type: "string"}, + {Code: "vdcGroupUid", Field: "VdcGroupUid", Type: "string"}, + {Code: "vdcType", Field: "VdcType", Type: "string"}, + {Code: "vdcUid", Field: "VdcUid", Type: "string"}, + {Code: "virtualServicesCount", Field: "VirtualServicesCount", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !newState.VdcGroupUid.IsNull() && !newState.VdcGroupUid.IsUnknown() { + resolvedVdcGroupUid, err := r.client.ResolveRefSvcParamValue(ctx, 29, newState.VdcGroupUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_group_uid", err.Error()) + } else if resolvedVdcGroupUid != "" && resolvedVdcGroupUid != newState.VdcGroupUid.ValueString() { + newState.VdcGroupUid = types.StringValue(resolvedVdcGroupUid) + } + } + if !newState.VdcUid.IsNull() && !newState.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, newState.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error()) + } else if resolvedVdcUid != "" && resolvedVdcUid != newState.VdcUid.ValueString() { + newState.VdcUid = types.StringValue(resolvedVdcUid) + } + } + // Restore user-provided casing в state (Read). + // При чтении у нас нет plan — но предыдущий state уже хранит значение + // в регистре пользователя (после первого Create оно было восстановлено). + // Берём prior state (переменная state) как эталон регистра. + // Если API вернул то же UUID только строчными буквами — восстанавливаем. + if !state.VdcGroupUid.IsNull() && !state.VdcGroupUid.IsUnknown() && !newState.VdcGroupUid.IsNull() && !newState.VdcGroupUid.IsUnknown() { + if strings.EqualFold(newState.VdcGroupUid.ValueString(), state.VdcGroupUid.ValueString()) { + newState.VdcGroupUid = state.VdcGroupUid + } + } + if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() && !newState.VdcUid.IsNull() && !newState.VdcUid.IsUnknown() { + if strings.EqualFold(newState.VdcUid.ValueString(), state.VdcUid.ValueString()) { + newState.VdcUid = state.VdcUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *VcNsxtResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VcNsxtModel + var state VcNsxtModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.NeedEnableAVI.IsNull() != state.NeedEnableAVI.IsNull() || plan.NeedEnableAVI.IsUnknown() != state.NeedEnableAVI.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.NeedEnableAVI.IsNull() && !plan.NeedEnableAVI.IsUnknown() && !state.NeedEnableAVI.IsNull() && !state.NeedEnableAVI.IsUnknown() { + if plan.NeedEnableAVI.ValueBool() != state.NeedEnableAVI.ValueBool() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.VirtualServicesCount.IsNull() != state.VirtualServicesCount.IsNull() || plan.VirtualServicesCount.IsUnknown() != state.VirtualServicesCount.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.VirtualServicesCount.IsNull() && !plan.VirtualServicesCount.IsUnknown() && !state.VirtualServicesCount.IsNull() && !state.VirtualServicesCount.IsUnknown() { + if plan.VirtualServicesCount.ValueInt64() != state.VirtualServicesCount.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.IpSpaceName.IsNull() != state.IpSpaceName.IsNull() || plan.IpSpaceName.IsUnknown() != state.IpSpaceName.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.IpSpaceName.IsNull() && !plan.IpSpaceName.IsUnknown() && !state.IpSpaceName.IsNull() && !state.IpSpaceName.IsUnknown() { + if !strings.EqualFold(plan.IpSpaceName.ValueString(), state.IpSpaceName.ValueString()) { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.QosProfile.IsNull() != state.QosProfile.IsNull() || plan.QosProfile.IsUnknown() != state.QosProfile.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.QosProfile.IsNull() && !plan.QosProfile.IsUnknown() && !state.QosProfile.IsNull() && !state.QosProfile.IsUnknown() { + if !strings.EqualFold(plan.QosProfile.ValueString(), state.QosProfile.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 368: resources_core.FormatBool(plan.NeedEnableAVI), + 369: resources_core.FormatInt64(plan.VirtualServicesCount), + 372: resources_core.FormatString(plan.IpSpaceName), + 856: resources_core.FormatString(plan.QosProfile), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 22, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "needEnableAVI", Field: "NeedEnableAVI", Type: "bool"}, + {Code: "qosProfile", Field: "QosProfile", Type: "string"}, + {Code: "vdcGroupUid", Field: "VdcGroupUid", Type: "string"}, + {Code: "vdcType", Field: "VdcType", Type: "string"}, + {Code: "vdcUid", Field: "VdcUid", Type: "string"}, + {Code: "virtualServicesCount", Field: "VirtualServicesCount", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.VdcGroupUid.IsNull() && !state.VdcGroupUid.IsUnknown() { + resolvedVdcGroupUid, err := r.client.ResolveRefSvcParamValue(ctx, 29, state.VdcGroupUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_group_uid", err.Error()) + } else if resolvedVdcGroupUid != "" && resolvedVdcGroupUid != state.VdcGroupUid.ValueString() { + state.VdcGroupUid = types.StringValue(resolvedVdcGroupUid) + } + } + if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, state.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error()) + } else if resolvedVdcUid != "" && resolvedVdcUid != state.VdcUid.ValueString() { + state.VdcUid = types.StringValue(resolvedVdcUid) + } + } + // Restore user-provided casing в state (Update). + // После API-вызова modify state содержит значения в lower-case от API. + // Plan == config == то что написал пользователь (регистр неизменён). + // EqualFold: если UUID совпадает без учёта регистра — берём из plan. + if !plan.VdcGroupUid.IsNull() && !plan.VdcGroupUid.IsUnknown() && !state.VdcGroupUid.IsNull() && !state.VdcGroupUid.IsUnknown() { + if strings.EqualFold(state.VdcGroupUid.ValueString(), plan.VdcGroupUid.ValueString()) { + state.VdcGroupUid = plan.VdcGroupUid + } + } + if !plan.VdcUid.IsNull() && !plan.VdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + if strings.EqualFold(state.VdcUid.ValueString(), plan.VdcUid.ValueString()) { + state.VdcUid = plan.VdcUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcNsxtResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VcNsxtModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *VcNsxtResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *VcNsxtResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/23_vc_vm_resource.go b/generated/dev/go/23_vc_vm_resource.go new file mode 100644 index 0000000..d39e2e1 --- /dev/null +++ b/generated/dev/go/23_vc_vm_resource.go @@ -0,0 +1,581 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vc_vm +// Service ID: 23 + +var _ resource.Resource = &VcVmResource{} +var _ resource.ResourceWithModifyPlan = &VcVmResource{} +var _ resource.ResourceWithImportState = &VcVmResource{} + +type VcVmResource struct { + client *core.UniversalClient +} + +type VcVmModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessIpList types.String `tfsdk:"access_ip_list"` + IpSpaceName types.String `tfsdk:"ip_space_name"` + TemplateName types.String `tfsdk:"template_name"` + VappUid types.String `tfsdk:"vapp_uid"` + VmCpu types.String `tfsdk:"vm_cpu"` + VmDisksize types.String `tfsdk:"vm_disksize"` + VmName types.String `tfsdk:"vm_name"` + VmRam types.String `tfsdk:"vm_ram"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewVcVmResource() resource.Resource { + return &VcVmResource{} +} + +func (r *VcVmResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vc_vm" +} + +func (r *VcVmResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_ip_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("79.111.119.78/32 185.247.187.1/32"), MarkdownDescription: "Вставить содержимое из 'curl ifconfig.io'
IP адреса в CIDR формате через пробел для настройки аксес листов"}, + "ip_space_name": schema.StringAttribute{Required: true, MarkdownDescription: "Брать из свойств ресурсной платформы
.vcd.ipSpace

Имя IP-space для выделения внешнего IP (IP выделяется обязательно) Не изменяется после создания."}, + "template_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("RockyLinux_9-16G"), MarkdownDescription: "ОС Не изменяется после создания."}, + "vapp_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "InstanceUid vApp. vApp должен быть создан Не изменяется после создания."}, + "vm_cpu": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("2"), MarkdownDescription: "CPU"}, + "vm_disksize": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("10"), MarkdownDescription: "Размер диска под сервис (GB)"}, + "vm_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("vm0001"), MarkdownDescription: "Имя ВМ. Не поддерживает спец-символы Не изменяется после создания."}, + "vm_ram": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("2"), MarkdownDescription: "RAM (GB)"}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VcVmResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *VcVmModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *VcVmModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan VcVmModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan. + // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов. + // Resolve (displayName → UUID или uppercase → lowercase) нужен только для + // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка + // "Provider produced invalid plan: planned value does not match config value". + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.IpSpaceName.IsNull() && !plan.IpSpaceName.IsUnknown() && !state.IpSpaceName.IsNull() && !state.IpSpaceName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.IpSpaceName.ValueString(), state.IpSpaceName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить ip_space_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.TemplateName.IsNull() && !plan.TemplateName.IsUnknown() && !state.TemplateName.IsNull() && !state.TemplateName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.TemplateName.ValueString(), state.TemplateName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить template_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.VappUid.IsNull() && !plan.VappUid.IsUnknown() && !state.VappUid.IsNull() && !state.VappUid.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VappUid.ValueString(), state.VappUid.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить vapp_uid", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.VmName.IsNull() && !plan.VmName.IsUnknown() && !state.VmName.IsNull() && !state.VmName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VmName.ValueString(), state.VmName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить vm_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.IpSpaceName.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "ip_space_name is required.") + return + } + if config.IpSpaceName.IsUnknown() { + return + } + if config.VappUid.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "vapp_uid is required.") + return + } + if config.VappUid.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + if !config.VappUid.IsNull() && !config.VappUid.IsUnknown() { + resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, config.VappUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error()) + } else if resolvedVappUid != "" && resolvedVappUid != config.VappUid.ValueString() { + config.VappUid = types.StringValue(resolvedVappUid) + } + } + params := map[int]string{ + 33: resources_core.FormatString(config.VappUid), + 35: resources_core.FormatString(config.VmName), + 36: resources_core.FormatString(config.VmCpu), + 37: resources_core.FormatString(config.VmRam), + 38: resources_core.FormatString(config.VmDisksize), + 176: resources_core.FormatString(config.TemplateName), + 177: resources_core.FormatString(config.AccessIpList), + 406: resources_core.FormatString(config.IpSpaceName), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 23, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *VcVmResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data VcVmModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.IpSpaceName.IsNull() || data.IpSpaceName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "ip_space_name is required.") + return + } + if data.VappUid.IsNull() || data.VappUid.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "vapp_uid is required.") + return + } + // ══════════════════════════════════════════════════════════════════════════ + // ПРАВИЛО TERRAFORM (официальная документация): + // «If an attribute value is configured, it is NEVER valid to change that + // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что + // написал пользователь). Менять plan запрещено на уровне фреймворка. + // + // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре. + // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный). + // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит: + // «Provider produced inconsistent result after apply». + // + // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот. + // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как + // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API). + // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный + // регистр в state через strings.EqualFold (сравниваем без учёта регистра). + // ══════════════════════════════════════════════════════════════════════════ + originalVappUid := data.VappUid + if !data.VappUid.IsNull() && !data.VappUid.IsUnknown() { + resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, data.VappUid.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if resolvedVappUid != "" && resolvedVappUid != data.VappUid.ValueString() { + data.VappUid = types.StringValue(resolvedVappUid) + } + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 23, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 33: resources_core.FormatString(data.VappUid), + 35: resources_core.FormatString(data.VmName), + 36: resources_core.FormatString(data.VmCpu), + 37: resources_core.FormatString(data.VmRam), + 38: resources_core.FormatString(data.VmDisksize), + 176: resources_core.FormatString(data.TemplateName), + 177: resources_core.FormatString(data.AccessIpList), + 406: resources_core.FormatString(data.IpSpaceName), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 23, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 23, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessIpList", Field: "AccessIpList", Type: "string"}, + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "templateName", Field: "TemplateName", Type: "string"}, + {Code: "vappUid", Field: "VappUid", Type: "string"}, + {Code: "vmCpu", Field: "VmCpu", Type: "string"}, + {Code: "vmDisksize", Field: "VmDisksize", Type: "string"}, + {Code: "vmName", Field: "VmName", Type: "string"}, + {Code: "vmRam", Field: "VmRam", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.VappUid.IsNull() && !state.VappUid.IsUnknown() { + resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, state.VappUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error()) + } else if resolvedVappUid != "" && resolvedVappUid != state.VappUid.ValueString() { + state.VappUid = types.StringValue(resolvedVappUid) + } + } + // Restore user-provided casing в state (Create). + // EqualFold = «совпадают ли значения без учёта регистра?» + // Если да — значит API вернул «туже» строку, только в другом регистре. + // Заменяем state на original (то что было в plan/config пользователя). + // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint. + if !originalVappUid.IsNull() && !originalVappUid.IsUnknown() && !state.VappUid.IsNull() && !state.VappUid.IsUnknown() { + if strings.EqualFold(state.VappUid.ValueString(), originalVappUid.ValueString()) { + state.VappUid = originalVappUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcVmResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VcVmModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 23, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessIpList", Field: "AccessIpList", Type: "string"}, + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "templateName", Field: "TemplateName", Type: "string"}, + {Code: "vappUid", Field: "VappUid", Type: "string"}, + {Code: "vmCpu", Field: "VmCpu", Type: "string"}, + {Code: "vmDisksize", Field: "VmDisksize", Type: "string"}, + {Code: "vmName", Field: "VmName", Type: "string"}, + {Code: "vmRam", Field: "VmRam", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !newState.VappUid.IsNull() && !newState.VappUid.IsUnknown() { + resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, newState.VappUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error()) + } else if resolvedVappUid != "" && resolvedVappUid != newState.VappUid.ValueString() { + newState.VappUid = types.StringValue(resolvedVappUid) + } + } + // Restore user-provided casing в state (Read). + // При чтении у нас нет plan — но предыдущий state уже хранит значение + // в регистре пользователя (после первого Create оно было восстановлено). + // Берём prior state (переменная state) как эталон регистра. + // Если API вернул то же UUID только строчными буквами — восстанавливаем. + if !state.VappUid.IsNull() && !state.VappUid.IsUnknown() && !newState.VappUid.IsNull() && !newState.VappUid.IsUnknown() { + if strings.EqualFold(newState.VappUid.ValueString(), state.VappUid.ValueString()) { + newState.VappUid = state.VappUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *VcVmResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VcVmModel + var state VcVmModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.VmCpu.IsNull() != state.VmCpu.IsNull() || plan.VmCpu.IsUnknown() != state.VmCpu.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.VmCpu.IsNull() && !plan.VmCpu.IsUnknown() && !state.VmCpu.IsNull() && !state.VmCpu.IsUnknown() { + if !strings.EqualFold(plan.VmCpu.ValueString(), state.VmCpu.ValueString()) { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.VmRam.IsNull() != state.VmRam.IsNull() || plan.VmRam.IsUnknown() != state.VmRam.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.VmRam.IsNull() && !plan.VmRam.IsUnknown() && !state.VmRam.IsNull() && !state.VmRam.IsUnknown() { + if !strings.EqualFold(plan.VmRam.ValueString(), state.VmRam.ValueString()) { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.VmDisksize.IsNull() != state.VmDisksize.IsNull() || plan.VmDisksize.IsUnknown() != state.VmDisksize.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.VmDisksize.IsNull() && !plan.VmDisksize.IsUnknown() && !state.VmDisksize.IsNull() && !state.VmDisksize.IsUnknown() { + if !strings.EqualFold(plan.VmDisksize.ValueString(), state.VmDisksize.ValueString()) { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.AccessIpList.IsNull() != state.AccessIpList.IsNull() || plan.AccessIpList.IsUnknown() != state.AccessIpList.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.AccessIpList.IsNull() && !plan.AccessIpList.IsUnknown() && !state.AccessIpList.IsNull() && !state.AccessIpList.IsUnknown() { + if !strings.EqualFold(plan.AccessIpList.ValueString(), state.AccessIpList.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 186: resources_core.FormatString(plan.VmCpu), + 187: resources_core.FormatString(plan.VmRam), + 188: resources_core.FormatString(plan.VmDisksize), + 189: resources_core.FormatString(plan.AccessIpList), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 23, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessIpList", Field: "AccessIpList", Type: "string"}, + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "templateName", Field: "TemplateName", Type: "string"}, + {Code: "vappUid", Field: "VappUid", Type: "string"}, + {Code: "vmCpu", Field: "VmCpu", Type: "string"}, + {Code: "vmDisksize", Field: "VmDisksize", Type: "string"}, + {Code: "vmName", Field: "VmName", Type: "string"}, + {Code: "vmRam", Field: "VmRam", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.VappUid.IsNull() && !state.VappUid.IsUnknown() { + resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, state.VappUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error()) + } else if resolvedVappUid != "" && resolvedVappUid != state.VappUid.ValueString() { + state.VappUid = types.StringValue(resolvedVappUid) + } + } + // Restore user-provided casing в state (Update). + // После API-вызова modify state содержит значения в lower-case от API. + // Plan == config == то что написал пользователь (регистр неизменён). + // EqualFold: если UUID совпадает без учёта регистра — берём из plan. + if !plan.VappUid.IsNull() && !plan.VappUid.IsUnknown() && !state.VappUid.IsNull() && !state.VappUid.IsUnknown() { + if strings.EqualFold(state.VappUid.ValueString(), plan.VappUid.ValueString()) { + state.VappUid = plan.VappUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcVmResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VcVmModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *VcVmResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *VcVmResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/24_vc_nat_resource.go b/generated/dev/go/24_vc_nat_resource.go new file mode 100644 index 0000000..20fe4bd --- /dev/null +++ b/generated/dev/go/24_vc_nat_resource.go @@ -0,0 +1,322 @@ +package resources_gen + +import ( + "context" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vc_nat +// Service ID: 24 + +var _ resource.Resource = &VcNatResource{} +var _ resource.ResourceWithModifyPlan = &VcNatResource{} +var _ resource.ResourceWithImportState = &VcNatResource{} + +type VcNatResource struct { + client *core.UniversalClient +} + +type VcNatModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewVcNatResource() resource.Resource { + return &VcNatResource{} +} + +func (r *VcNatResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vc_nat" +} + +func (r *VcNatResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VcNatResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *VcNatModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *VcNatModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan VcNatModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{} + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 24, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *VcNatResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data VcNatModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 24, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{} + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 24, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 24, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{}) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcNatResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VcNatModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 24, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{}) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *VcNatResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VcNatModel + var state VcNatModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 24, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{}) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcNatResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VcNatModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *VcNatResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *VcNatResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/25_vcexternalip_resource.go b/generated/dev/go/25_vcexternalip_resource.go new file mode 100644 index 0000000..c375621 --- /dev/null +++ b/generated/dev/go/25_vcexternalip_resource.go @@ -0,0 +1,604 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vcexternalip +// Service ID: 25 + +var _ resource.Resource = &VcexternalipResource{} +var _ resource.ResourceWithModifyPlan = &VcexternalipResource{} +var _ resource.ResourceWithImportState = &VcexternalipResource{} + +type VcexternalipResource struct { + client *core.UniversalClient +} + +type VcexternalipModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + DnatCreate types.Bool `tfsdk:"dnat_create"` + FromServiceCloudEdgeName types.String `tfsdk:"from_service_cloud_edge_name"` + FromServiceCloudEdgeScope types.String `tfsdk:"from_service_cloud_edge_scope"` + FromServiceCloudOrgName types.String `tfsdk:"from_service_cloud_org_name"` + FromServiceCloudVdcName types.String `tfsdk:"from_service_cloud_vdc_name"` + FromServiceCloudVmwareUrl types.String `tfsdk:"from_service_cloud_vmware_url"` + FromServiceNamespace types.String `tfsdk:"from_service_namespace"` + FromServiceVdcGroupName types.String `tfsdk:"from_service_vdc_group_name"` + InternalAddrAccess types.String `tfsdk:"internal_addr_access"` + InternalPortAccess types.String `tfsdk:"internal_port_access"` + IpSpaceName types.String `tfsdk:"ip_space_name"` + ResourceRealm types.String `tfsdk:"resource_realm"` + ServiceUid types.String `tfsdk:"service_uid"` + SnatCreate types.Bool `tfsdk:"snat_create"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewVcexternalipResource() resource.Resource { + return &VcexternalipResource{} +} + +func (r *VcexternalipResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vcexternalip" +} + +func (r *VcexternalipResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "dnat_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Необходимо создать DNAT-правило."}, + "from_service_cloud_edge_name": schema.StringAttribute{Optional: true, MarkdownDescription: "Edge составной услуги (указывать, если resourceRealm связан с CloudDirector) Не изменяется после создания."}, + "from_service_cloud_edge_scope": schema.StringAttribute{Optional: true, MarkdownDescription: "Не изменяется после создания."}, + "from_service_cloud_org_name": schema.StringAttribute{Optional: true, MarkdownDescription: "Org составной услуги (указывать, если resourceRealm связан с CloudDirector) Не изменяется после создания."}, + "from_service_cloud_vdc_name": schema.StringAttribute{Optional: true, MarkdownDescription: "Vdc составной услуги (указывать, если resourceRealm связан с CloudDirector) Не изменяется после создания."}, + "from_service_cloud_vmware_url": schema.StringAttribute{Optional: true, MarkdownDescription: "Api-Url составной услуги (указывать, если resourceRealm связан с CloudDirector) Не изменяется после создания."}, + "from_service_namespace": schema.StringAttribute{Required: true, MarkdownDescription: "Namespace составной услуги (для отображения правил FW) Не изменяется после создания."}, + "from_service_vdc_group_name": schema.StringAttribute{Optional: true, MarkdownDescription: "Не изменяется после создания."}, + "internal_addr_access": schema.StringAttribute{Required: true, MarkdownDescription: "Внутренний адрес для связки ната. Требует dnatCreate=true или snatCreate=true"}, + "internal_port_access": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("5432"), MarkdownDescription: "Порт для связки ната с адресом. Несколько значений можно передавать через пробел. Если передать all, то dnat будет на все порты. Требует dnatCreate=true
"}, + "ip_space_name": schema.StringAttribute{Required: true, MarkdownDescription: "Тип ip Space (internet-ipv4-v1 / internet-antiddos-v1 / internet-no-antiddos-v1)"}, + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("sandbox.nubes.ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + "service_uid": schema.StringAttribute{Required: true}, + "snat_create": schema.BoolAttribute{Optional: true, MarkdownDescription: "Необходимо создать SNAT-правило.
"}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VcexternalipResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *VcexternalipModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *VcexternalipModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan VcexternalipModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.FromServiceCloudEdgeName.IsNull() && !plan.FromServiceCloudEdgeName.IsUnknown() && !state.FromServiceCloudEdgeName.IsNull() && !state.FromServiceCloudEdgeName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.FromServiceCloudEdgeName.ValueString(), state.FromServiceCloudEdgeName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить from_service_cloud_edge_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.FromServiceCloudEdgeScope.IsNull() && !plan.FromServiceCloudEdgeScope.IsUnknown() && !state.FromServiceCloudEdgeScope.IsNull() && !state.FromServiceCloudEdgeScope.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.FromServiceCloudEdgeScope.ValueString(), state.FromServiceCloudEdgeScope.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить from_service_cloud_edge_scope", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.FromServiceCloudOrgName.IsNull() && !plan.FromServiceCloudOrgName.IsUnknown() && !state.FromServiceCloudOrgName.IsNull() && !state.FromServiceCloudOrgName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.FromServiceCloudOrgName.ValueString(), state.FromServiceCloudOrgName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить from_service_cloud_org_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.FromServiceCloudVdcName.IsNull() && !plan.FromServiceCloudVdcName.IsUnknown() && !state.FromServiceCloudVdcName.IsNull() && !state.FromServiceCloudVdcName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.FromServiceCloudVdcName.ValueString(), state.FromServiceCloudVdcName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить from_service_cloud_vdc_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.FromServiceCloudVmwareUrl.IsNull() && !plan.FromServiceCloudVmwareUrl.IsUnknown() && !state.FromServiceCloudVmwareUrl.IsNull() && !state.FromServiceCloudVmwareUrl.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.FromServiceCloudVmwareUrl.ValueString(), state.FromServiceCloudVmwareUrl.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить from_service_cloud_vmware_url", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.FromServiceNamespace.IsNull() && !plan.FromServiceNamespace.IsUnknown() && !state.FromServiceNamespace.IsNull() && !state.FromServiceNamespace.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.FromServiceNamespace.ValueString(), state.FromServiceNamespace.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить from_service_namespace", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.FromServiceVdcGroupName.IsNull() && !plan.FromServiceVdcGroupName.IsUnknown() && !state.FromServiceVdcGroupName.IsNull() && !state.FromServiceVdcGroupName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.FromServiceVdcGroupName.ValueString(), state.FromServiceVdcGroupName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить from_service_vdc_group_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.FromServiceNamespace.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "from_service_namespace is required.") + return + } + if config.FromServiceNamespace.IsUnknown() { + return + } + if config.InternalAddrAccess.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "internal_addr_access is required.") + return + } + if config.InternalAddrAccess.IsUnknown() { + return + } + if config.IpSpaceName.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "ip_space_name is required.") + return + } + if config.IpSpaceName.IsUnknown() { + return + } + if config.ServiceUid.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "service_uid is required.") + return + } + if config.ServiceUid.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 140: resources_core.FormatString(config.ServiceUid), + 141: resources_core.FormatBool(config.DnatCreate), + 142: resources_core.FormatString(config.InternalPortAccess), + 143: resources_core.FormatBool(config.SnatCreate), + 144: resources_core.FormatString(config.InternalAddrAccess), + 324: resources_core.FormatString(config.FromServiceNamespace), + 325: resources_core.FormatString(config.FromServiceCloudEdgeName), + 326: resources_core.FormatString(config.FromServiceCloudVdcName), + 327: resources_core.FormatString(config.FromServiceCloudOrgName), + 328: resources_core.FormatString(config.FromServiceCloudVmwareUrl), + 336: resources_core.FormatString(config.IpSpaceName), + 488: resources_core.FormatString(config.ResourceRealm), + 624: resources_core.FormatString(config.FromServiceCloudEdgeScope), + 626: resources_core.FormatString(config.FromServiceVdcGroupName), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 25, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *VcexternalipResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data VcexternalipModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.FromServiceNamespace.IsNull() || data.FromServiceNamespace.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "from_service_namespace is required.") + return + } + if data.InternalAddrAccess.IsNull() || data.InternalAddrAccess.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "internal_addr_access is required.") + return + } + if data.IpSpaceName.IsNull() || data.IpSpaceName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "ip_space_name is required.") + return + } + if data.ServiceUid.IsNull() || data.ServiceUid.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "service_uid is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 25, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 140: resources_core.FormatString(data.ServiceUid), + 141: resources_core.FormatBool(data.DnatCreate), + 142: resources_core.FormatString(data.InternalPortAccess), + 143: resources_core.FormatBool(data.SnatCreate), + 144: resources_core.FormatString(data.InternalAddrAccess), + 324: resources_core.FormatString(data.FromServiceNamespace), + 325: resources_core.FormatString(data.FromServiceCloudEdgeName), + 326: resources_core.FormatString(data.FromServiceCloudVdcName), + 327: resources_core.FormatString(data.FromServiceCloudOrgName), + 328: resources_core.FormatString(data.FromServiceCloudVmwareUrl), + 336: resources_core.FormatString(data.IpSpaceName), + 488: resources_core.FormatString(data.ResourceRealm), + 624: resources_core.FormatString(data.FromServiceCloudEdgeScope), + 626: resources_core.FormatString(data.FromServiceVdcGroupName), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 25, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 25, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "dnatCreate", Field: "DnatCreate", Type: "bool"}, + {Code: "fromServiceCloudEdgeName", Field: "FromServiceCloudEdgeName", Type: "string"}, + {Code: "fromServiceCloudEdgeScope", Field: "FromServiceCloudEdgeScope", Type: "string"}, + {Code: "fromServiceCloudOrgName", Field: "FromServiceCloudOrgName", Type: "string"}, + {Code: "fromServiceCloudVdcName", Field: "FromServiceCloudVdcName", Type: "string"}, + {Code: "fromServiceCloudVmwareUrl", Field: "FromServiceCloudVmwareUrl", Type: "string"}, + {Code: "fromServiceNamespace", Field: "FromServiceNamespace", Type: "string"}, + {Code: "fromServiceVdcGroupName", Field: "FromServiceVdcGroupName", Type: "string"}, + {Code: "internalAddrAccess", Field: "InternalAddrAccess", Type: "string"}, + {Code: "internalPortAccess", Field: "InternalPortAccess", Type: "string"}, + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "serviceUid", Field: "ServiceUid", Type: "string"}, + {Code: "snatCreate", Field: "SnatCreate", Type: "bool"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcexternalipResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VcexternalipModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 25, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "dnatCreate", Field: "DnatCreate", Type: "bool"}, + {Code: "fromServiceCloudEdgeName", Field: "FromServiceCloudEdgeName", Type: "string"}, + {Code: "fromServiceCloudEdgeScope", Field: "FromServiceCloudEdgeScope", Type: "string"}, + {Code: "fromServiceCloudOrgName", Field: "FromServiceCloudOrgName", Type: "string"}, + {Code: "fromServiceCloudVdcName", Field: "FromServiceCloudVdcName", Type: "string"}, + {Code: "fromServiceCloudVmwareUrl", Field: "FromServiceCloudVmwareUrl", Type: "string"}, + {Code: "fromServiceNamespace", Field: "FromServiceNamespace", Type: "string"}, + {Code: "fromServiceVdcGroupName", Field: "FromServiceVdcGroupName", Type: "string"}, + {Code: "internalAddrAccess", Field: "InternalAddrAccess", Type: "string"}, + {Code: "internalPortAccess", Field: "InternalPortAccess", Type: "string"}, + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "serviceUid", Field: "ServiceUid", Type: "string"}, + {Code: "snatCreate", Field: "SnatCreate", Type: "bool"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *VcexternalipResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VcexternalipModel + var state VcexternalipModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.ServiceUid.IsNull() != state.ServiceUid.IsNull() || plan.ServiceUid.IsUnknown() != state.ServiceUid.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ServiceUid.IsNull() && !plan.ServiceUid.IsUnknown() && !state.ServiceUid.IsNull() && !state.ServiceUid.IsUnknown() { + if !strings.EqualFold(plan.ServiceUid.ValueString(), state.ServiceUid.ValueString()) { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.IpSpaceName.IsNull() != state.IpSpaceName.IsNull() || plan.IpSpaceName.IsUnknown() != state.IpSpaceName.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.IpSpaceName.IsNull() && !plan.IpSpaceName.IsUnknown() && !state.IpSpaceName.IsNull() && !state.IpSpaceName.IsUnknown() { + if !strings.EqualFold(plan.IpSpaceName.ValueString(), state.IpSpaceName.ValueString()) { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.DnatCreate.IsNull() != state.DnatCreate.IsNull() || plan.DnatCreate.IsUnknown() != state.DnatCreate.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.DnatCreate.IsNull() && !plan.DnatCreate.IsUnknown() && !state.DnatCreate.IsNull() && !state.DnatCreate.IsUnknown() { + if plan.DnatCreate.ValueBool() != state.DnatCreate.ValueBool() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.SnatCreate.IsNull() != state.SnatCreate.IsNull() || plan.SnatCreate.IsUnknown() != state.SnatCreate.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.SnatCreate.IsNull() && !plan.SnatCreate.IsUnknown() && !state.SnatCreate.IsNull() && !state.SnatCreate.IsUnknown() { + if plan.SnatCreate.ValueBool() != state.SnatCreate.ValueBool() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.InternalPortAccess.IsNull() != state.InternalPortAccess.IsNull() || plan.InternalPortAccess.IsUnknown() != state.InternalPortAccess.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.InternalPortAccess.IsNull() && !plan.InternalPortAccess.IsUnknown() && !state.InternalPortAccess.IsNull() && !state.InternalPortAccess.IsUnknown() { + if !strings.EqualFold(plan.InternalPortAccess.ValueString(), state.InternalPortAccess.ValueString()) { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.InternalAddrAccess.IsNull() != state.InternalAddrAccess.IsNull() || plan.InternalAddrAccess.IsUnknown() != state.InternalAddrAccess.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.InternalAddrAccess.IsNull() && !plan.InternalAddrAccess.IsUnknown() && !state.InternalAddrAccess.IsNull() && !state.InternalAddrAccess.IsUnknown() { + if !strings.EqualFold(plan.InternalAddrAccess.ValueString(), state.InternalAddrAccess.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 627: resources_core.FormatString(plan.ServiceUid), + 628: resources_core.FormatString(plan.IpSpaceName), + 629: resources_core.FormatBool(plan.DnatCreate), + 630: resources_core.FormatBool(plan.SnatCreate), + 631: resources_core.FormatString(plan.InternalPortAccess), + 632: resources_core.FormatString(plan.InternalAddrAccess), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 25, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "dnatCreate", Field: "DnatCreate", Type: "bool"}, + {Code: "fromServiceCloudEdgeName", Field: "FromServiceCloudEdgeName", Type: "string"}, + {Code: "fromServiceCloudEdgeScope", Field: "FromServiceCloudEdgeScope", Type: "string"}, + {Code: "fromServiceCloudOrgName", Field: "FromServiceCloudOrgName", Type: "string"}, + {Code: "fromServiceCloudVdcName", Field: "FromServiceCloudVdcName", Type: "string"}, + {Code: "fromServiceCloudVmwareUrl", Field: "FromServiceCloudVmwareUrl", Type: "string"}, + {Code: "fromServiceNamespace", Field: "FromServiceNamespace", Type: "string"}, + {Code: "fromServiceVdcGroupName", Field: "FromServiceVdcGroupName", Type: "string"}, + {Code: "internalAddrAccess", Field: "InternalAddrAccess", Type: "string"}, + {Code: "internalPortAccess", Field: "InternalPortAccess", Type: "string"}, + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "serviceUid", Field: "ServiceUid", Type: "string"}, + {Code: "snatCreate", Field: "SnatCreate", Type: "bool"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcexternalipResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VcexternalipModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *VcexternalipResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *VcexternalipResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/26_vapp_resource.go b/generated/dev/go/26_vapp_resource.go new file mode 100644 index 0000000..e210b7f --- /dev/null +++ b/generated/dev/go/26_vapp_resource.go @@ -0,0 +1,571 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vapp +// Service ID: 26 + +var _ resource.Resource = &VappResource{} +var _ resource.ResourceWithModifyPlan = &VappResource{} +var _ resource.ResourceWithImportState = &VappResource{} + +type VappResource struct { + client *core.UniversalClient +} + +type VappModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + NsxtUid types.String `tfsdk:"nsxt_uid"` + VappName types.String `tfsdk:"vapp_name"` + VdcUid types.String `tfsdk:"vdc_uid"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewVappResource() resource.Resource { + return &VappResource{} +} + +func (r *VappResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vapp" +} + +func (r *VappResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "nsxt_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UUID Услуги `Сетевой шлюз периметра (Edge)` Не изменяется после создания."}, + "vapp_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("9pv2jybu"), MarkdownDescription: "Уникальное в рамках организации Не изменяется после создания."}, + "vdc_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UUID Услуги `Виртуальный датацентр (vDC)` Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VappResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *VappModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *VappModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan VappModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan. + // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов. + // Resolve (displayName → UUID или uppercase → lowercase) нужен только для + // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка + // "Provider produced invalid plan: planned value does not match config value". + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.NsxtUid.IsNull() && !plan.NsxtUid.IsUnknown() && !state.NsxtUid.IsNull() && !state.NsxtUid.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.NsxtUid.ValueString(), state.NsxtUid.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить nsxt_uid", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.VappName.IsNull() && !plan.VappName.IsUnknown() && !state.VappName.IsNull() && !state.VappName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VappName.ValueString(), state.VappName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить vapp_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.VdcUid.IsNull() && !plan.VdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VdcUid.ValueString(), state.VdcUid.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить vdc_uid", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.NsxtUid.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "nsxt_uid is required.") + return + } + if config.NsxtUid.IsUnknown() { + return + } + if config.VdcUid.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "vdc_uid is required.") + return + } + if config.VdcUid.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + if !config.NsxtUid.IsNull() && !config.NsxtUid.IsUnknown() { + resolvedNsxtUid, err := r.client.ResolveRefSvcParamValue(ctx, 22, config.NsxtUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve nsxt_uid", err.Error()) + } else if resolvedNsxtUid != "" && resolvedNsxtUid != config.NsxtUid.ValueString() { + config.NsxtUid = types.StringValue(resolvedNsxtUid) + } + } + if !config.VdcUid.IsNull() && !config.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, config.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error()) + } else if resolvedVdcUid != "" && resolvedVdcUid != config.VdcUid.ValueString() { + config.VdcUid = types.StringValue(resolvedVdcUid) + } + } + params := map[int]string{ + 190: resources_core.FormatString(config.NsxtUid), + 191: resources_core.FormatString(config.VappName), + 623: resources_core.FormatString(config.VdcUid), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 26, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *VappResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data VappModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.NsxtUid.IsNull() || data.NsxtUid.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "nsxt_uid is required.") + return + } + if data.VdcUid.IsNull() || data.VdcUid.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "vdc_uid is required.") + return + } + // ══════════════════════════════════════════════════════════════════════════ + // ПРАВИЛО TERRAFORM (официальная документация): + // «If an attribute value is configured, it is NEVER valid to change that + // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что + // написал пользователь). Менять plan запрещено на уровне фреймворка. + // + // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре. + // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный). + // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит: + // «Provider produced inconsistent result after apply». + // + // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот. + // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как + // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API). + // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный + // регистр в state через strings.EqualFold (сравниваем без учёта регистра). + // ══════════════════════════════════════════════════════════════════════════ + originalNsxtUid := data.NsxtUid + // ══════════════════════════════════════════════════════════════════════════ + // ПРАВИЛО TERRAFORM (официальная документация): + // «If an attribute value is configured, it is NEVER valid to change that + // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что + // написал пользователь). Менять plan запрещено на уровне фреймворка. + // + // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре. + // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный). + // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит: + // «Provider produced inconsistent result after apply». + // + // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот. + // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как + // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API). + // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный + // регистр в state через strings.EqualFold (сравниваем без учёта регистра). + // ══════════════════════════════════════════════════════════════════════════ + originalVdcUid := data.VdcUid + if !data.NsxtUid.IsNull() && !data.NsxtUid.IsUnknown() { + resolvedNsxtUid, err := r.client.ResolveRefSvcParamValue(ctx, 22, data.NsxtUid.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if resolvedNsxtUid != "" && resolvedNsxtUid != data.NsxtUid.ValueString() { + data.NsxtUid = types.StringValue(resolvedNsxtUid) + } + } + if !data.VdcUid.IsNull() && !data.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, data.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if resolvedVdcUid != "" && resolvedVdcUid != data.VdcUid.ValueString() { + data.VdcUid = types.StringValue(resolvedVdcUid) + } + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 26, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 190: resources_core.FormatString(data.NsxtUid), + 191: resources_core.FormatString(data.VappName), + 623: resources_core.FormatString(data.VdcUid), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 26, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 26, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "nsxtUid", Field: "NsxtUid", Type: "string"}, + {Code: "vappName", Field: "VappName", Type: "string"}, + {Code: "vdcUid", Field: "VdcUid", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.NsxtUid.IsNull() && !state.NsxtUid.IsUnknown() { + resolvedNsxtUid, err := r.client.ResolveRefSvcParamValue(ctx, 22, state.NsxtUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve nsxt_uid", err.Error()) + } else if resolvedNsxtUid != "" && resolvedNsxtUid != state.NsxtUid.ValueString() { + state.NsxtUid = types.StringValue(resolvedNsxtUid) + } + } + if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, state.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error()) + } else if resolvedVdcUid != "" && resolvedVdcUid != state.VdcUid.ValueString() { + state.VdcUid = types.StringValue(resolvedVdcUid) + } + } + // Restore user-provided casing в state (Create). + // EqualFold = «совпадают ли значения без учёта регистра?» + // Если да — значит API вернул «туже» строку, только в другом регистре. + // Заменяем state на original (то что было в plan/config пользователя). + // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint. + if !originalNsxtUid.IsNull() && !originalNsxtUid.IsUnknown() && !state.NsxtUid.IsNull() && !state.NsxtUid.IsUnknown() { + if strings.EqualFold(state.NsxtUid.ValueString(), originalNsxtUid.ValueString()) { + state.NsxtUid = originalNsxtUid + } + } + if !originalVdcUid.IsNull() && !originalVdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + if strings.EqualFold(state.VdcUid.ValueString(), originalVdcUid.ValueString()) { + state.VdcUid = originalVdcUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VappResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VappModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 26, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "nsxtUid", Field: "NsxtUid", Type: "string"}, + {Code: "vappName", Field: "VappName", Type: "string"}, + {Code: "vdcUid", Field: "VdcUid", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !newState.NsxtUid.IsNull() && !newState.NsxtUid.IsUnknown() { + resolvedNsxtUid, err := r.client.ResolveRefSvcParamValue(ctx, 22, newState.NsxtUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve nsxt_uid", err.Error()) + } else if resolvedNsxtUid != "" && resolvedNsxtUid != newState.NsxtUid.ValueString() { + newState.NsxtUid = types.StringValue(resolvedNsxtUid) + } + } + if !newState.VdcUid.IsNull() && !newState.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, newState.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error()) + } else if resolvedVdcUid != "" && resolvedVdcUid != newState.VdcUid.ValueString() { + newState.VdcUid = types.StringValue(resolvedVdcUid) + } + } + // Restore user-provided casing в state (Read). + // При чтении у нас нет plan — но предыдущий state уже хранит значение + // в регистре пользователя (после первого Create оно было восстановлено). + // Берём prior state (переменная state) как эталон регистра. + // Если API вернул то же UUID только строчными буквами — восстанавливаем. + if !state.NsxtUid.IsNull() && !state.NsxtUid.IsUnknown() && !newState.NsxtUid.IsNull() && !newState.NsxtUid.IsUnknown() { + if strings.EqualFold(newState.NsxtUid.ValueString(), state.NsxtUid.ValueString()) { + newState.NsxtUid = state.NsxtUid + } + } + if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() && !newState.VdcUid.IsNull() && !newState.VdcUid.IsUnknown() { + if strings.EqualFold(newState.VdcUid.ValueString(), state.VdcUid.ValueString()) { + newState.VdcUid = state.VdcUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *VappResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VappModel + var state VappModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 26, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "nsxtUid", Field: "NsxtUid", Type: "string"}, + {Code: "vappName", Field: "VappName", Type: "string"}, + {Code: "vdcUid", Field: "VdcUid", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.NsxtUid.IsNull() && !state.NsxtUid.IsUnknown() { + resolvedNsxtUid, err := r.client.ResolveRefSvcParamValue(ctx, 22, state.NsxtUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve nsxt_uid", err.Error()) + } else if resolvedNsxtUid != "" && resolvedNsxtUid != state.NsxtUid.ValueString() { + state.NsxtUid = types.StringValue(resolvedNsxtUid) + } + } + if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, state.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error()) + } else if resolvedVdcUid != "" && resolvedVdcUid != state.VdcUid.ValueString() { + state.VdcUid = types.StringValue(resolvedVdcUid) + } + } + // Restore user-provided casing в state (Update). + // После API-вызова modify state содержит значения в lower-case от API. + // Plan == config == то что написал пользователь (регистр неизменён). + // EqualFold: если UUID совпадает без учёта регистра — берём из plan. + if !plan.NsxtUid.IsNull() && !plan.NsxtUid.IsUnknown() && !state.NsxtUid.IsNull() && !state.NsxtUid.IsUnknown() { + if strings.EqualFold(state.NsxtUid.ValueString(), plan.NsxtUid.ValueString()) { + state.NsxtUid = plan.NsxtUid + } + } + if !plan.VdcUid.IsNull() && !plan.VdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + if strings.EqualFold(state.VdcUid.ValueString(), plan.VdcUid.ValueString()) { + state.VdcUid = plan.VdcUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VappResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VappModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *VappResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *VappResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/27_vc_vm_v2_resource.go b/generated/dev/go/27_vc_vm_v2_resource.go new file mode 100644 index 0000000..6a05554 --- /dev/null +++ b/generated/dev/go/27_vc_vm_v2_resource.go @@ -0,0 +1,350 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vc_vm_v2 +// Service ID: 27 + +var _ resource.Resource = &VcVmV2Resource{} +var _ resource.ResourceWithModifyPlan = &VcVmV2Resource{} +var _ resource.ResourceWithImportState = &VcVmV2Resource{} + +type VcVmV2Resource struct { + client *core.UniversalClient +} + +type VcVmV2Model struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + ServerListJson types.String `tfsdk:"server_list_json"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewVcVmV2Resource() resource.Resource { + return &VcVmV2Resource{} +} + +func (r *VcVmV2Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vc_vm_v2" +} + +func (r *VcVmV2Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "server_list_json": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Необходимо изменить vappUid и ipSpaceName
Пример конфигурации взять по ссылке ниже

Объект серверов вида [{}] (Map(Object))
Более подробнее смотреть в README.md (https://gitea.mgmt.nubes.ru/cloudServices/vcVmV2)", PlanModifiers: []planmodifier.String{resources_core.JsonNormalize()}}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VcVmV2Resource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *VcVmV2Model + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *VcVmV2Model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan VcVmV2Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 301: resources_core.FormatString(config.ServerListJson), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 27, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *VcVmV2Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data VcVmV2Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 27, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 301: resources_core.FormatString(data.ServerListJson), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 27, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 27, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "serverListJson", Field: "ServerListJson", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcVmV2Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VcVmV2Model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 27, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "serverListJson", Field: "ServerListJson", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *VcVmV2Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VcVmV2Model + var state VcVmV2Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.ServerListJson.IsNull() != state.ServerListJson.IsNull() || plan.ServerListJson.IsUnknown() != state.ServerListJson.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ServerListJson.IsNull() && !plan.ServerListJson.IsUnknown() && !state.ServerListJson.IsNull() && !state.ServerListJson.IsUnknown() { + if !strings.EqualFold(plan.ServerListJson.ValueString(), state.ServerListJson.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 302: resources_core.FormatString(plan.ServerListJson), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 27, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "serverListJson", Field: "ServerListJson", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcVmV2Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VcVmV2Model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *VcVmV2Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *VcVmV2Resource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/28_vc_vm_v3_resource.go b/generated/dev/go/28_vc_vm_v3_resource.go new file mode 100644 index 0000000..61fe97c --- /dev/null +++ b/generated/dev/go/28_vc_vm_v3_resource.go @@ -0,0 +1,697 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vc_vm_v3 +// Service ID: 28 + +var _ resource.Resource = &VcVmV3Resource{} +var _ resource.ResourceWithModifyPlan = &VcVmV3Resource{} +var _ resource.ResourceWithImportState = &VcVmV3Resource{} + +type VcVmV3Resource struct { + client *core.UniversalClient +} + +type VcVmV3Model struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + // --- Redeploy support (ARCHITECTURE.md) --- + // git_revision: при изменении вызывает redeploy вместо modify. + // Опциональное поле — если не задано, modify работает как обычно. + GitRevision types.String `tfsdk:"git_revision"` + AccessIpList types.String `tfsdk:"access_ip_list"` + AccessPortList types.String `tfsdk:"access_port_list"` + CloudInit types.String `tfsdk:"cloud_init"` + ImageVm types.String `tfsdk:"image_vm"` + IpSpaceName types.String `tfsdk:"ip_space_name"` + UserLogin types.String `tfsdk:"user_login"` + UserPublicKey types.String `tfsdk:"user_public_key"` + VappUid types.String `tfsdk:"vapp_uid"` + VmCpu types.Int64 `tfsdk:"vm_cpu"` + VmDisk types.Int64 `tfsdk:"vm_disk"` + VmName types.String `tfsdk:"vm_name"` + VmRam types.Int64 `tfsdk:"vm_ram"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewVcVmV3Resource() resource.Resource { + return &VcVmV3Resource{} +} + +func (r *VcVmV3Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vc_vm_v3" +} + +func (r *VcVmV3Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_ip_list": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "Формат массива
Каким адресам будет предоставлен доступ до вм

Требует выделенного внешнего IP
Если ничего не передано, то выставлено ["0.0.0.0/0"]", PlanModifiers: []planmodifier.String{resources_core.JsonNormalize()}}, + "access_port_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[\"22\"]"), MarkdownDescription: "По каким портам открыть доступ к ВМ.
Порты 22, 10050 проставляются автоматически
Пока доступно:
22 - SSH,
80 - HTTP,
443 - HTTPS,
10050 - ZABBIX"}, + "cloud_init": schema.StringAttribute{Optional: true, MarkdownDescription: "Можно передать создание дополнительных пользователей.
На текущий момент не реализовано выполнения в cloud-init, установки пакетов, выполнения команд и записи файлов. Не изменяется после создания."}, + "image_vm": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания."}, + "ip_space_name": schema.StringAttribute{Required: true}, + "user_login": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("myuser"), MarkdownDescription: "Учётка SSH Не изменяется после создания."}, + "user_public_key": schema.StringAttribute{Required: true, MarkdownDescription: "Публичная часть SSH-Ключа для пользователя Не изменяется после создания."}, + "vapp_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UI display_name for service_id=26 (mapped to UUID in core) Не изменяется после создания."}, + "vm_cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "шт"}, + "vm_disk": schema.Int64Attribute{Optional: true, MarkdownDescription: "Основной диск у vm зависит от образа (см. документацию)
Если необходимо дополнительное место для vm, введите колличество Gb в эту форму"}, + "vm_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("web01"), MarkdownDescription: "Не изменяется после создания."}, + "vm_ram": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Gb"}, + // --- Redeploy support (ARCHITECTURE.md) --- + // При изменении git_revision вызывается redeploy вместо modify. + // Если не задано — modify работает без изменений. + "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VcVmV3Resource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *VcVmV3Model + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *VcVmV3Model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan VcVmV3Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan. + // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов. + // Resolve (displayName → UUID или uppercase → lowercase) нужен только для + // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка + // "Provider produced invalid plan: planned value does not match config value". + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.CloudInit.IsNull() && !plan.CloudInit.IsUnknown() && !state.CloudInit.IsNull() && !state.CloudInit.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.CloudInit.ValueString(), state.CloudInit.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить cloud_init", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ImageVm.IsNull() && !plan.ImageVm.IsUnknown() && !state.ImageVm.IsNull() && !state.ImageVm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ImageVm.ValueString(), state.ImageVm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить image_vm", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.UserLogin.IsNull() && !plan.UserLogin.IsUnknown() && !state.UserLogin.IsNull() && !state.UserLogin.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.UserLogin.ValueString(), state.UserLogin.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить user_login", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.UserPublicKey.IsNull() && !plan.UserPublicKey.IsUnknown() && !state.UserPublicKey.IsNull() && !state.UserPublicKey.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.UserPublicKey.ValueString(), state.UserPublicKey.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить user_public_key", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.VappUid.IsNull() && !plan.VappUid.IsUnknown() && !state.VappUid.IsNull() && !state.VappUid.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VappUid.ValueString(), state.VappUid.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить vapp_uid", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.VmName.IsNull() && !plan.VmName.IsUnknown() && !state.VmName.IsNull() && !state.VmName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VmName.ValueString(), state.VmName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить vm_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.ImageVm.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "image_vm is required.") + return + } + if config.ImageVm.IsUnknown() { + return + } + if config.IpSpaceName.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "ip_space_name is required.") + return + } + if config.IpSpaceName.IsUnknown() { + return + } + if config.UserPublicKey.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "user_public_key is required.") + return + } + if config.UserPublicKey.IsUnknown() { + return + } + if config.VappUid.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "vapp_uid is required.") + return + } + if config.VappUid.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + if !config.VappUid.IsNull() && !config.VappUid.IsUnknown() { + resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, config.VappUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error()) + } else if resolvedVappUid != "" && resolvedVappUid != config.VappUid.ValueString() { + config.VappUid = types.StringValue(resolvedVappUid) + } + } + params := map[int]string{ + 407: resources_core.FormatString(config.VappUid), + 408: resources_core.FormatString(config.VmName), + 409: resources_core.FormatInt64(config.VmCpu), + 410: resources_core.FormatInt64(config.VmRam), + 411: resources_core.FormatInt64(config.VmDisk), + 412: resources_core.FormatString(config.IpSpaceName), + 413: resources_core.FormatString(config.AccessIpList), + 414: resources_core.FormatString(config.ImageVm), + 415: resources_core.FormatString(config.CloudInit), + 416: resources_core.FormatString(config.UserLogin), + 417: resources_core.FormatString(config.UserPublicKey), + 448: resources_core.FormatString(config.AccessPortList), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 28, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *VcVmV3Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data VcVmV3Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.ImageVm.IsNull() || data.ImageVm.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "image_vm is required.") + return + } + if data.IpSpaceName.IsNull() || data.IpSpaceName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "ip_space_name is required.") + return + } + if data.UserPublicKey.IsNull() || data.UserPublicKey.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "user_public_key is required.") + return + } + if data.VappUid.IsNull() || data.VappUid.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "vapp_uid is required.") + return + } + // ══════════════════════════════════════════════════════════════════════════ + // ПРАВИЛО TERRAFORM (официальная документация): + // «If an attribute value is configured, it is NEVER valid to change that + // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что + // написал пользователь). Менять plan запрещено на уровне фреймворка. + // + // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре. + // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный). + // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит: + // «Provider produced inconsistent result after apply». + // + // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот. + // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как + // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API). + // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный + // регистр в state через strings.EqualFold (сравниваем без учёта регистра). + // ══════════════════════════════════════════════════════════════════════════ + originalVappUid := data.VappUid + if !data.VappUid.IsNull() && !data.VappUid.IsUnknown() { + resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, data.VappUid.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if resolvedVappUid != "" && resolvedVappUid != data.VappUid.ValueString() { + data.VappUid = types.StringValue(resolvedVappUid) + } + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 28, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 407: resources_core.FormatString(data.VappUid), + 408: resources_core.FormatString(data.VmName), + 409: resources_core.FormatInt64(data.VmCpu), + 410: resources_core.FormatInt64(data.VmRam), + 411: resources_core.FormatInt64(data.VmDisk), + 412: resources_core.FormatString(data.IpSpaceName), + 413: resources_core.FormatString(data.AccessIpList), + 414: resources_core.FormatString(data.ImageVm), + 415: resources_core.FormatString(data.CloudInit), + 416: resources_core.FormatString(data.UserLogin), + 417: resources_core.FormatString(data.UserPublicKey), + 448: resources_core.FormatString(data.AccessPortList), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 28, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 28, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessIpList", Field: "AccessIpList", Type: "string"}, + {Code: "accessPortList", Field: "AccessPortList", Type: "string"}, + {Code: "cloudInit", Field: "CloudInit", Type: "string"}, + {Code: "imageVm", Field: "ImageVm", Type: "string"}, + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "userLogin", Field: "UserLogin", Type: "string"}, + {Code: "userPublicKey", Field: "UserPublicKey", Type: "string"}, + {Code: "vappUid", Field: "VappUid", Type: "string"}, + {Code: "vmCpu", Field: "VmCpu", Type: "int64"}, + {Code: "vmDisk", Field: "VmDisk", Type: "int64"}, + {Code: "vmName", Field: "VmName", Type: "string"}, + {Code: "vmRam", Field: "VmRam", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.VappUid.IsNull() && !state.VappUid.IsUnknown() { + resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, state.VappUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error()) + } else if resolvedVappUid != "" && resolvedVappUid != state.VappUid.ValueString() { + state.VappUid = types.StringValue(resolvedVappUid) + } + } + // Restore user-provided casing в state (Create). + // EqualFold = «совпадают ли значения без учёта регистра?» + // Если да — значит API вернул «туже» строку, только в другом регистре. + // Заменяем state на original (то что было в plan/config пользователя). + // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint. + if !originalVappUid.IsNull() && !originalVappUid.IsUnknown() && !state.VappUid.IsNull() && !state.VappUid.IsUnknown() { + if strings.EqualFold(state.VappUid.ValueString(), originalVappUid.ValueString()) { + state.VappUid = originalVappUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcVmV3Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VcVmV3Model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 28, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessIpList", Field: "AccessIpList", Type: "string"}, + {Code: "accessPortList", Field: "AccessPortList", Type: "string"}, + {Code: "cloudInit", Field: "CloudInit", Type: "string"}, + {Code: "imageVm", Field: "ImageVm", Type: "string"}, + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "userLogin", Field: "UserLogin", Type: "string"}, + {Code: "userPublicKey", Field: "UserPublicKey", Type: "string"}, + {Code: "vappUid", Field: "VappUid", Type: "string"}, + {Code: "vmCpu", Field: "VmCpu", Type: "int64"}, + {Code: "vmDisk", Field: "VmDisk", Type: "int64"}, + {Code: "vmName", Field: "VmName", Type: "string"}, + {Code: "vmRam", Field: "VmRam", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !newState.VappUid.IsNull() && !newState.VappUid.IsUnknown() { + resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, newState.VappUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error()) + } else if resolvedVappUid != "" && resolvedVappUid != newState.VappUid.ValueString() { + newState.VappUid = types.StringValue(resolvedVappUid) + } + } + // Restore user-provided casing в state (Read). + // При чтении у нас нет plan — но предыдущий state уже хранит значение + // в регистре пользователя (после первого Create оно было восстановлено). + // Берём prior state (переменная state) как эталон регистра. + // Если API вернул то же UUID только строчными буквами — восстанавливаем. + if !state.VappUid.IsNull() && !state.VappUid.IsUnknown() && !newState.VappUid.IsNull() && !newState.VappUid.IsUnknown() { + if strings.EqualFold(newState.VappUid.ValueString(), state.VappUid.ValueString()) { + newState.VappUid = state.VappUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *VcVmV3Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VcVmV3Model + var state VcVmV3Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.VmCpu.IsNull() != state.VmCpu.IsNull() || plan.VmCpu.IsUnknown() != state.VmCpu.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.VmCpu.IsNull() && !plan.VmCpu.IsUnknown() && !state.VmCpu.IsNull() && !state.VmCpu.IsUnknown() { + if plan.VmCpu.ValueInt64() != state.VmCpu.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.VmRam.IsNull() != state.VmRam.IsNull() || plan.VmRam.IsUnknown() != state.VmRam.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.VmRam.IsNull() && !plan.VmRam.IsUnknown() && !state.VmRam.IsNull() && !state.VmRam.IsUnknown() { + if plan.VmRam.ValueInt64() != state.VmRam.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.VmDisk.IsNull() != state.VmDisk.IsNull() || plan.VmDisk.IsUnknown() != state.VmDisk.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.VmDisk.IsNull() && !plan.VmDisk.IsUnknown() && !state.VmDisk.IsNull() && !state.VmDisk.IsUnknown() { + if plan.VmDisk.ValueInt64() != state.VmDisk.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.IpSpaceName.IsNull() != state.IpSpaceName.IsNull() || plan.IpSpaceName.IsUnknown() != state.IpSpaceName.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.IpSpaceName.IsNull() && !plan.IpSpaceName.IsUnknown() && !state.IpSpaceName.IsNull() && !state.IpSpaceName.IsUnknown() { + if !strings.EqualFold(plan.IpSpaceName.ValueString(), state.IpSpaceName.ValueString()) { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.AccessIpList.IsNull() != state.AccessIpList.IsNull() || plan.AccessIpList.IsUnknown() != state.AccessIpList.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.AccessIpList.IsNull() && !plan.AccessIpList.IsUnknown() && !state.AccessIpList.IsNull() && !state.AccessIpList.IsUnknown() { + if !strings.EqualFold(plan.AccessIpList.ValueString(), state.AccessIpList.ValueString()) { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.AccessPortList.IsNull() != state.AccessPortList.IsNull() || plan.AccessPortList.IsUnknown() != state.AccessPortList.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.AccessPortList.IsNull() && !plan.AccessPortList.IsUnknown() && !state.AccessPortList.IsNull() && !state.AccessPortList.IsUnknown() { + if !strings.EqualFold(plan.AccessPortList.ValueString(), state.AccessPortList.ValueString()) { + hasServiceParamChanges = true + } + } + } + // --- Redeploy support (ARCHITECTURE.md) --- + // Проверяем изменился ли git_revision. + // Если да — вызываем redeploy вместо modify. + // git_revision = "" (не задано) → обычный modify. + redeployRequested := false + if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() { + if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() { + redeployRequested = true + } + } + + if !hasServiceParamChanges && !redeployRequested { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + // --- Redeploy support (ARCHITECTURE.md) --- + if redeployRequested { + // Вызываем redeploy — пустой params (операция не требует CFS-параметров) + if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + } else { + params := map[int]string{ + 493: resources_core.FormatInt64(plan.VmCpu), + 494: resources_core.FormatInt64(plan.VmRam), + 495: resources_core.FormatInt64(plan.VmDisk), + 496: resources_core.FormatString(plan.IpSpaceName), + 497: resources_core.FormatString(plan.AccessIpList), + 498: resources_core.FormatString(plan.AccessPortList), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 28, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessIpList", Field: "AccessIpList", Type: "string"}, + {Code: "accessPortList", Field: "AccessPortList", Type: "string"}, + {Code: "cloudInit", Field: "CloudInit", Type: "string"}, + {Code: "imageVm", Field: "ImageVm", Type: "string"}, + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "userLogin", Field: "UserLogin", Type: "string"}, + {Code: "userPublicKey", Field: "UserPublicKey", Type: "string"}, + {Code: "vappUid", Field: "VappUid", Type: "string"}, + {Code: "vmCpu", Field: "VmCpu", Type: "int64"}, + {Code: "vmDisk", Field: "VmDisk", Type: "int64"}, + {Code: "vmName", Field: "VmName", Type: "string"}, + {Code: "vmRam", Field: "VmRam", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.VappUid.IsNull() && !state.VappUid.IsUnknown() { + resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, state.VappUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error()) + } else if resolvedVappUid != "" && resolvedVappUid != state.VappUid.ValueString() { + state.VappUid = types.StringValue(resolvedVappUid) + } + } + // Restore user-provided casing в state (Update). + // После API-вызова modify state содержит значения в lower-case от API. + // Plan == config == то что написал пользователь (регистр неизменён). + // EqualFold: если UUID совпадает без учёта регистра — берём из plan. + if !plan.VappUid.IsNull() && !plan.VappUid.IsUnknown() && !state.VappUid.IsNull() && !state.VappUid.IsUnknown() { + if strings.EqualFold(state.VappUid.ValueString(), plan.VappUid.ValueString()) { + state.VappUid = plan.VappUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcVmV3Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VcVmV3Model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *VcVmV3Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *VcVmV3Resource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/29_vc_vdc_group_resource.go b/generated/dev/go/29_vc_vdc_group_resource.go new file mode 100644 index 0000000..41864ef --- /dev/null +++ b/generated/dev/go/29_vc_vdc_group_resource.go @@ -0,0 +1,449 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vc_vdc_group +// Service ID: 29 + +var _ resource.Resource = &VcVdcGroupResource{} +var _ resource.ResourceWithModifyPlan = &VcVdcGroupResource{} +var _ resource.ResourceWithImportState = &VcVdcGroupResource{} + +type VcVdcGroupResource struct { + client *core.UniversalClient +} + +type VcVdcGroupModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + VdcUid types.String `tfsdk:"vdc_uid"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewVcVdcGroupResource() resource.Resource { + return &VcVdcGroupResource{} +} + +func (r *VcVdcGroupResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vc_vdc_group" +} + +func (r *VcVdcGroupResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "vdc_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UI display_name for service_id=21 (mapped to UUID in core) Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VcVdcGroupResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *VcVdcGroupModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *VcVdcGroupModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan VcVdcGroupModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan. + // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов. + // Resolve (displayName → UUID или uppercase → lowercase) нужен только для + // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка + // "Provider produced invalid plan: planned value does not match config value". + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.VdcUid.IsNull() && !plan.VdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VdcUid.ValueString(), state.VdcUid.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить vdc_uid", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.VdcUid.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "vdc_uid is required.") + return + } + if config.VdcUid.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + if !config.VdcUid.IsNull() && !config.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, config.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error()) + } else if resolvedVdcUid != "" && resolvedVdcUid != config.VdcUid.ValueString() { + config.VdcUid = types.StringValue(resolvedVdcUid) + } + } + params := map[int]string{ + 618: resources_core.FormatString(config.VdcUid), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 29, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *VcVdcGroupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data VcVdcGroupModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.VdcUid.IsNull() || data.VdcUid.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "vdc_uid is required.") + return + } + // ══════════════════════════════════════════════════════════════════════════ + // ПРАВИЛО TERRAFORM (официальная документация): + // «If an attribute value is configured, it is NEVER valid to change that + // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что + // написал пользователь). Менять plan запрещено на уровне фреймворка. + // + // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре. + // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный). + // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит: + // «Provider produced inconsistent result after apply». + // + // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот. + // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как + // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API). + // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный + // регистр в state через strings.EqualFold (сравниваем без учёта регистра). + // ══════════════════════════════════════════════════════════════════════════ + originalVdcUid := data.VdcUid + if !data.VdcUid.IsNull() && !data.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, data.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if resolvedVdcUid != "" && resolvedVdcUid != data.VdcUid.ValueString() { + data.VdcUid = types.StringValue(resolvedVdcUid) + } + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 29, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 618: resources_core.FormatString(data.VdcUid), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 29, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 29, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "vdcUid", Field: "VdcUid", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, state.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error()) + } else if resolvedVdcUid != "" && resolvedVdcUid != state.VdcUid.ValueString() { + state.VdcUid = types.StringValue(resolvedVdcUid) + } + } + // Restore user-provided casing в state (Create). + // EqualFold = «совпадают ли значения без учёта регистра?» + // Если да — значит API вернул «туже» строку, только в другом регистре. + // Заменяем state на original (то что было в plan/config пользователя). + // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint. + if !originalVdcUid.IsNull() && !originalVdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + if strings.EqualFold(state.VdcUid.ValueString(), originalVdcUid.ValueString()) { + state.VdcUid = originalVdcUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcVdcGroupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VcVdcGroupModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 29, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "vdcUid", Field: "VdcUid", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !newState.VdcUid.IsNull() && !newState.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, newState.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error()) + } else if resolvedVdcUid != "" && resolvedVdcUid != newState.VdcUid.ValueString() { + newState.VdcUid = types.StringValue(resolvedVdcUid) + } + } + // Restore user-provided casing в state (Read). + // При чтении у нас нет plan — но предыдущий state уже хранит значение + // в регистре пользователя (после первого Create оно было восстановлено). + // Берём prior state (переменная state) как эталон регистра. + // Если API вернул то же UUID только строчными буквами — восстанавливаем. + if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() && !newState.VdcUid.IsNull() && !newState.VdcUid.IsUnknown() { + if strings.EqualFold(newState.VdcUid.ValueString(), state.VdcUid.ValueString()) { + newState.VdcUid = state.VdcUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *VcVdcGroupResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VcVdcGroupModel + var state VcVdcGroupModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 29, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "vdcUid", Field: "VdcUid", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, state.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error()) + } else if resolvedVdcUid != "" && resolvedVdcUid != state.VdcUid.ValueString() { + state.VdcUid = types.StringValue(resolvedVdcUid) + } + } + // Restore user-provided casing в state (Update). + // После API-вызова modify state содержит значения в lower-case от API. + // Plan == config == то что написал пользователь (регистр неизменён). + // EqualFold: если UUID совпадает без учёта регистра — берём из plan. + if !plan.VdcUid.IsNull() && !plan.VdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + if strings.EqualFold(state.VdcUid.ValueString(), plan.VdcUid.ValueString()) { + state.VdcUid = plan.VdcUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcVdcGroupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VcVdcGroupModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *VcVdcGroupResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *VcVdcGroupResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/29_vc_vdc_group_vdc_resource.go b/generated/dev/go/29_vc_vdc_group_vdc_resource.go new file mode 100644 index 0000000..3e3e8ef --- /dev/null +++ b/generated/dev/go/29_vc_vdc_group_vdc_resource.go @@ -0,0 +1,326 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vc_vdc_group +// Subresource: vdc + +var _ resource.Resource = &VcVdcGroupVdcResource{} + +// VcVdcGroupVdcResource manages a subresource via operations. +type VcVdcGroupVdcResource struct { + client *core.UniversalClient +} + +type VcVdcGroupVdcModel struct { + ID types.String `tfsdk:"id"` + VcVdcGroupID types.String `tfsdk:"vc_vdc_group_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` +} + +func NewVcVdcGroupVdcResource() resource.Resource { + return &VcVdcGroupVdcResource{} +} + +func (r *VcVdcGroupVdcResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vc_vdc_group_vdc" +} + +func (r *VcVdcGroupVdcResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "vc_vdc_group_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VcVdcGroupVdcResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan VcVdcGroupVdcModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.VcVdcGroupID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{} + identityCodes := []string{} + listKey, idKey := resources_core.ResolveSubresourceStateKeys("vdc", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "vdc", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{}) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "vdc", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "vdc", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "vdc", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *VcVdcGroupVdcResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VcVdcGroupVdcModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcVdcGroupVdcResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VcVdcGroupVdcModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state VcVdcGroupVdcModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.VcVdcGroupID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{} + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "vdc", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *VcVdcGroupVdcResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VcVdcGroupVdcModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + return + } + + instanceUID := strings.TrimSpace(state.VcVdcGroupID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{} + identityCodes := []string{} + listKey, idKey := resources_core.ResolveSubresourceStateKeys("vdc", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{}) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *VcVdcGroupVdcResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/2_template_resource.go b/generated/dev/go/2_template_resource.go new file mode 100644 index 0000000..d3e71ae --- /dev/null +++ b/generated/dev/go/2_template_resource.go @@ -0,0 +1,397 @@ +package resources_gen + +import ( + "context" + "fmt" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: template +// Service ID: 2 + +var _ resource.Resource = &TemplateResource{} +var _ resource.ResourceWithModifyPlan = &TemplateResource{} +var _ resource.ResourceWithImportState = &TemplateResource{} + +type TemplateResource struct { + client *core.UniversalClient +} + +// TemplateAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration. +type TemplateAccessConfigurationModel struct { + SlaveAccessList types.String `tfsdk:"slave_access_list" json:"slaveAccessList"` + Type types.String `tfsdk:"type" json:"type"` + Domain types.String `tfsdk:"domain" json:"domain"` + MasterIpSpace types.String `tfsdk:"master_ip_space" json:"masterIpSpace"` + MasterAccessList types.String `tfsdk:"master_access_list" json:"masterAccessList"` + SlaveIpSpace types.String `tfsdk:"slave_ip_space" json:"slaveIpSpace"` +} + +// TemplateClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type TemplateClusterConfigurationModel struct { + Disk types.Int64 `tfsdk:"disk" json:"disk"` + Memory types.Int64 `tfsdk:"memory" json:"memory"` + Replicas types.Int64 `tfsdk:"replicas" json:"replicas"` + Cpu types.Int64 `tfsdk:"cpu" json:"cpu"` +} + +// TemplateStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type TemplateStartupConfigurationModel struct { + ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"` +} + +type TemplateModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessConfiguration *TemplateAccessConfigurationModel `tfsdk:"access_configuration"` + ClusterConfiguration *TemplateClusterConfigurationModel `tfsdk:"cluster_configuration"` + StartupConfiguration *TemplateStartupConfigurationModel `tfsdk:"startup_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewTemplateResource() resource.Resource { + return &TemplateResource{} +} + +func (r *TemplateResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_template" +} + +func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "slave_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]")}, + "type": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("ingress")}, + "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("abracadabra-mgonny3n")}, + "master_ip_space": schema.StringAttribute{Required: true}, + "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]")}, + "slave_ip_space": schema.StringAttribute{Required: true}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(5)}, + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(128)}, + "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)}, + "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100)}, + }}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "resource_realm": schema.StringAttribute{Required: true}, + }}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *TemplateResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *TemplateModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *TemplateModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan TemplateModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 813: resources_core.BuildJSON(map[string]string{"disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64())}), + 814: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}), + 815: resources_core.BuildJSON(map[string]string{"slaveAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.SlaveAccessList.ValueString()), "type": fmt.Sprintf("\"%s\"", config.AccessConfiguration.Type.ValueString()), "domain": fmt.Sprintf("\"%s\"", config.AccessConfiguration.Domain.ValueString()), "masterIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.SlaveIpSpace.ValueString())}), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 2, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *TemplateResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data TemplateModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 2, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 813: resources_core.BuildJSON(map[string]string{"disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64())}), + 814: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}), + 815: resources_core.BuildJSON(map[string]string{"slaveAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.SlaveAccessList.ValueString()), "type": fmt.Sprintf("\"%s\"", data.AccessConfiguration.Type.ValueString()), "domain": fmt.Sprintf("\"%s\"", data.AccessConfiguration.Domain.ValueString()), "masterIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.SlaveIpSpace.ValueString())}), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 2, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 2, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *TemplateResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state TemplateModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 2, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan TemplateModel + var state TemplateModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 816: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64())}), + 817: resources_core.BuildJSON(map[string]string{"masterAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.SlaveIpSpace.ValueString()), "slaveAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.SlaveAccessList.ValueString()), "type": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.Type.ValueString()), "domain": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.Domain.ValueString()), "masterIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterIpSpace.ValueString())}), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 2, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *TemplateResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state TemplateModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *TemplateResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *TemplateResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/32_vmpostgre_resource.go b/generated/dev/go/32_vmpostgre_resource.go new file mode 100644 index 0000000..5d39c90 --- /dev/null +++ b/generated/dev/go/32_vmpostgre_resource.go @@ -0,0 +1,514 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vmpostgre +// Service ID: 32 + +var _ resource.Resource = &VmpostgreResource{} +var _ resource.ResourceWithModifyPlan = &VmpostgreResource{} +var _ resource.ResourceWithImportState = &VmpostgreResource{} + +type VmpostgreResource struct { + client *core.UniversalClient +} + +type VmpostgreModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + ResourceRealm types.String `tfsdk:"resource_realm"` + UserDb types.String `tfsdk:"user_db"` + UserLogin types.String `tfsdk:"user_login"` + UserPassword types.String `tfsdk:"user_password"` + VmUid types.String `tfsdk:"vm_uid"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewVmpostgreResource() resource.Resource { + return &VmpostgreResource{} +} + +func (r *VmpostgreResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vmpostgre" +} + +func (r *VmpostgreResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("sandbox.nubes.ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + "user_db": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("servicedb"), MarkdownDescription: "Наименование Базы данных
Не изменяется после создания."}, + "user_login": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("serviceuser"), MarkdownDescription: "Пользователь базы данных
Не изменяется после создания."}, + "user_password": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("fj29gm48fm!d374mc9"), MarkdownDescription: "Пароль от доступа к Базе данных
Не изменяется после создания."}, + "vm_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "InstanceUid ВМ. ВМ должна быть создана
Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VmpostgreResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *VmpostgreModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *VmpostgreModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan VmpostgreModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan. + // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов. + // Resolve (displayName → UUID или uppercase → lowercase) нужен только для + // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка + // "Provider produced invalid plan: planned value does not match config value". + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.UserDb.IsNull() && !plan.UserDb.IsUnknown() && !state.UserDb.IsNull() && !state.UserDb.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.UserDb.ValueString(), state.UserDb.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить user_db", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.UserLogin.IsNull() && !plan.UserLogin.IsUnknown() && !state.UserLogin.IsNull() && !state.UserLogin.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.UserLogin.ValueString(), state.UserLogin.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить user_login", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.UserPassword.IsNull() && !plan.UserPassword.IsUnknown() && !state.UserPassword.IsNull() && !state.UserPassword.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.UserPassword.ValueString(), state.UserPassword.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить user_password", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.VmUid.IsNull() && !plan.VmUid.IsUnknown() && !state.VmUid.IsNull() && !state.VmUid.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VmUid.ValueString(), state.VmUid.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить vm_uid", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.VmUid.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "vm_uid is required.") + return + } + if config.VmUid.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + if !config.VmUid.IsNull() && !config.VmUid.IsUnknown() { + resolvedVmUid, err := r.client.ResolveRefSvcParamValue(ctx, 23, config.VmUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vm_uid", err.Error()) + } else if resolvedVmUid != "" && resolvedVmUid != config.VmUid.ValueString() { + config.VmUid = types.StringValue(resolvedVmUid) + } + } + params := map[int]string{ + 45: resources_core.FormatString(config.VmUid), + 46: resources_core.FormatString(config.UserDb), + 47: resources_core.FormatString(config.UserLogin), + 48: resources_core.FormatString(config.UserPassword), + 278: resources_core.FormatString(config.ResourceRealm), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 32, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *VmpostgreResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data VmpostgreModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.VmUid.IsNull() || data.VmUid.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "vm_uid is required.") + return + } + // ══════════════════════════════════════════════════════════════════════════ + // ПРАВИЛО TERRAFORM (официальная документация): + // «If an attribute value is configured, it is NEVER valid to change that + // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что + // написал пользователь). Менять plan запрещено на уровне фреймворка. + // + // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре. + // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный). + // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит: + // «Provider produced inconsistent result after apply». + // + // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот. + // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как + // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API). + // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный + // регистр в state через strings.EqualFold (сравниваем без учёта регистра). + // ══════════════════════════════════════════════════════════════════════════ + originalVmUid := data.VmUid + if !data.VmUid.IsNull() && !data.VmUid.IsUnknown() { + resolvedVmUid, err := r.client.ResolveRefSvcParamValue(ctx, 23, data.VmUid.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if resolvedVmUid != "" && resolvedVmUid != data.VmUid.ValueString() { + data.VmUid = types.StringValue(resolvedVmUid) + } + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 32, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 45: resources_core.FormatString(data.VmUid), + 46: resources_core.FormatString(data.UserDb), + 47: resources_core.FormatString(data.UserLogin), + 48: resources_core.FormatString(data.UserPassword), + 278: resources_core.FormatString(data.ResourceRealm), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 32, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 32, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "userDb", Field: "UserDb", Type: "string"}, + {Code: "userLogin", Field: "UserLogin", Type: "string"}, + {Code: "userPassword", Field: "UserPassword", Type: "string"}, + {Code: "vmUid", Field: "VmUid", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.VmUid.IsNull() && !state.VmUid.IsUnknown() { + resolvedVmUid, err := r.client.ResolveRefSvcParamValue(ctx, 23, state.VmUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vm_uid", err.Error()) + } else if resolvedVmUid != "" && resolvedVmUid != state.VmUid.ValueString() { + state.VmUid = types.StringValue(resolvedVmUid) + } + } + // Restore user-provided casing в state (Create). + // EqualFold = «совпадают ли значения без учёта регистра?» + // Если да — значит API вернул «туже» строку, только в другом регистре. + // Заменяем state на original (то что было в plan/config пользователя). + // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint. + if !originalVmUid.IsNull() && !originalVmUid.IsUnknown() && !state.VmUid.IsNull() && !state.VmUid.IsUnknown() { + if strings.EqualFold(state.VmUid.ValueString(), originalVmUid.ValueString()) { + state.VmUid = originalVmUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VmpostgreResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VmpostgreModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 32, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "userDb", Field: "UserDb", Type: "string"}, + {Code: "userLogin", Field: "UserLogin", Type: "string"}, + {Code: "userPassword", Field: "UserPassword", Type: "string"}, + {Code: "vmUid", Field: "VmUid", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !newState.VmUid.IsNull() && !newState.VmUid.IsUnknown() { + resolvedVmUid, err := r.client.ResolveRefSvcParamValue(ctx, 23, newState.VmUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vm_uid", err.Error()) + } else if resolvedVmUid != "" && resolvedVmUid != newState.VmUid.ValueString() { + newState.VmUid = types.StringValue(resolvedVmUid) + } + } + // Restore user-provided casing в state (Read). + // При чтении у нас нет plan — но предыдущий state уже хранит значение + // в регистре пользователя (после первого Create оно было восстановлено). + // Берём prior state (переменная state) как эталон регистра. + // Если API вернул то же UUID только строчными буквами — восстанавливаем. + if !state.VmUid.IsNull() && !state.VmUid.IsUnknown() && !newState.VmUid.IsNull() && !newState.VmUid.IsUnknown() { + if strings.EqualFold(newState.VmUid.ValueString(), state.VmUid.ValueString()) { + newState.VmUid = state.VmUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *VmpostgreResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VmpostgreModel + var state VmpostgreModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 32, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "userDb", Field: "UserDb", Type: "string"}, + {Code: "userLogin", Field: "UserLogin", Type: "string"}, + {Code: "userPassword", Field: "UserPassword", Type: "string"}, + {Code: "vmUid", Field: "VmUid", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.VmUid.IsNull() && !state.VmUid.IsUnknown() { + resolvedVmUid, err := r.client.ResolveRefSvcParamValue(ctx, 23, state.VmUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vm_uid", err.Error()) + } else if resolvedVmUid != "" && resolvedVmUid != state.VmUid.ValueString() { + state.VmUid = types.StringValue(resolvedVmUid) + } + } + // Restore user-provided casing в state (Update). + // После API-вызова modify state содержит значения в lower-case от API. + // Plan == config == то что написал пользователь (регистр неизменён). + // EqualFold: если UUID совпадает без учёта регистра — берём из plan. + if !plan.VmUid.IsNull() && !plan.VmUid.IsUnknown() && !state.VmUid.IsNull() && !state.VmUid.IsUnknown() { + if strings.EqualFold(state.VmUid.ValueString(), plan.VmUid.ValueString()) { + state.VmUid = plan.VmUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VmpostgreResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VmpostgreModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *VmpostgreResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *VmpostgreResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/50_nextcloud_backup_resource.go b/generated/dev/go/50_nextcloud_backup_resource.go new file mode 100644 index 0000000..feb1f0e --- /dev/null +++ b/generated/dev/go/50_nextcloud_backup_resource.go @@ -0,0 +1,344 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: nextcloud +// Subresource: backup + +var _ resource.Resource = &NextcloudBackupResource{} + +// NextcloudBackupResource manages a subresource via operations. +type NextcloudBackupResource struct { + client *core.UniversalClient +} + +type NextcloudBackupModel struct { + ID types.String `tfsdk:"id"` + NextcloudID types.String `tfsdk:"nextcloud_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + StartupConfiguration types.String `tfsdk:"startup_configuration"` +} + +func NewNextcloudBackupResource() resource.Resource { + return &NextcloudBackupResource{} +} + +func (r *NextcloudBackupResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_nextcloud_backup" +} + +func (r *NextcloudBackupResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "nextcloud_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "startup_configuration": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *NextcloudBackupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan NextcloudBackupModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if plan.StartupConfiguration.IsNull() || plan.StartupConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "startup_configuration is required.") + return + } + + instanceUID := strings.TrimSpace(plan.NextcloudID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "startupConfiguration": resources_core.FormatString(plan.StartupConfiguration), + } + identityCodes := []string{ + "startupConfiguration", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("backup", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "startupConfiguration": resources_core.FormatString(plan.StartupConfiguration), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_backup", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_backup", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *NextcloudBackupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state NextcloudBackupModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *NextcloudBackupResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan NextcloudBackupModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state NextcloudBackupModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.NextcloudID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{ + "startupConfiguration": resources_core.FormatString(plan.StartupConfiguration), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *NextcloudBackupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state NextcloudBackupModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + return + } + + instanceUID := strings.TrimSpace(state.NextcloudID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "startupConfiguration": resources_core.FormatString(state.StartupConfiguration), + } + identityCodes := []string{ + "startupConfiguration", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("backup", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{}) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *NextcloudBackupResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/50_nextcloud_resource.go b/generated/dev/go/50_nextcloud_resource.go new file mode 100644 index 0000000..3e5c608 --- /dev/null +++ b/generated/dev/go/50_nextcloud_resource.go @@ -0,0 +1,414 @@ +package resources_gen + +import ( + "context" + "fmt" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: nextcloud +// Service ID: 50 + +var _ resource.Resource = &NextcloudResource{} +var _ resource.ResourceWithModifyPlan = &NextcloudResource{} +var _ resource.ResourceWithImportState = &NextcloudResource{} + +type NextcloudResource struct { + client *core.UniversalClient +} + +// NextcloudAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration. +type NextcloudAccessConfigurationModel struct { + Domain types.String `tfsdk:"domain" json:"domain"` +} + +// NextcloudAppConfigurationModel — вложенная модель для map-fixed параметра appConfiguration. +type NextcloudAppConfigurationModel struct { + Version types.String `tfsdk:"version" json:"version"` + DocumentServer types.String `tfsdk:"document_server" json:"documentServer"` + LogLevel types.String `tfsdk:"log_level" json:"logLevel"` +} + +// NextcloudClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type NextcloudClusterConfigurationModel struct { + Cpu types.Int64 `tfsdk:"cpu" json:"cpu"` + Memory types.Int64 `tfsdk:"memory" json:"memory"` + Replicas types.Int64 `tfsdk:"replicas" json:"replicas"` + Disk types.Int64 `tfsdk:"disk" json:"disk"` +} + +// NextcloudStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type NextcloudStartupConfigurationModel struct { + ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"` + PsqlEnabled types.Bool `tfsdk:"psql_enabled" json:"psqlEnabled"` + RedisEnabled types.Bool `tfsdk:"redis_enabled" json:"redisEnabled"` + PsqlUid types.String `tfsdk:"psql_uid" json:"psqlUid"` + RedisUid types.String `tfsdk:"redis_uid" json:"redisUid"` +} + +type NextcloudModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessConfiguration *NextcloudAccessConfigurationModel `tfsdk:"access_configuration"` + AppConfiguration *NextcloudAppConfigurationModel `tfsdk:"app_configuration"` + ClusterConfiguration *NextcloudClusterConfigurationModel `tfsdk:"cluster_configuration"` + StartupConfiguration *NextcloudStartupConfigurationModel `tfsdk:"startup_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewNextcloudResource() resource.Resource { + return &NextcloudResource{} +} + +func (r *NextcloudResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_nextcloud" +} + +func (r *NextcloudResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("nextcloud-xcq3u70l"), MarkdownDescription: "Если указан FQDN в формате: `myapp.example.com`, предварительно будет проверяться DNS-запись (А-запись **должна смотреть на IP Ingress кластера ресурсной платформы**)

Если указан в формате `myapp`, DNS-запись будет автоматически создана в служебной DNS-зоне"}, + }}, + "app_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("33.0.4")}, + "document_server": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("collabora"), MarkdownDescription: "Выбор сервера документов (onlyoffice)/(collabora). onlyoffice пока не доступен
Размер пода collabora:
1.8 ядра. 2Gi ОЗУ"}, + "log_level": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("INFO"), MarkdownDescription: "Включает отображение логов самого nextcloud
Логи уходят в /dev/stdout контейнера
`NONE` отключает логирование"}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1000), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"}, + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1024), MarkdownDescription: "Указывается в Megabytes"}, + "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)}, + "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Указывается в GIGAbytes
На данном диске хранятся данные Nextcloud"}, + }}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"}, + "psql_enabled": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Инстанс обязан быть развёрнут на той же ресурсной платформе, где и будет располагаться Nextcloud"}, + "redis_enabled": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Инстанс обязан быть развёрнут на той же ресурсной платформе, где и будет располагаться Nextcloud"}, + "psql_uid": schema.StringAttribute{Required: true, MarkdownDescription: "Необходимо выбирать, когда выбран параметр `Подключить PostgreSQL из Личного кабинета`"}, + "redis_uid": schema.StringAttribute{Required: true, MarkdownDescription: "Необходимо выбирать, когда выбран параметр `Подключить Redis из Личного кабинета`"}, + }}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *NextcloudResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *NextcloudModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *NextcloudModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan NextcloudModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 1055: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString()), "psqlEnabled": fmt.Sprintf("%v", config.StartupConfiguration.PsqlEnabled.ValueBool()), "redisEnabled": fmt.Sprintf("%v", config.StartupConfiguration.RedisEnabled.ValueBool()), "psqlUid": fmt.Sprintf("\"%s\"", config.StartupConfiguration.PsqlUid.ValueString()), "redisUid": fmt.Sprintf("\"%s\"", config.StartupConfiguration.RedisUid.ValueString())}), + 1056: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64())}), + 1057: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", config.AccessConfiguration.Domain.ValueString())}), + 1058: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.AppConfiguration.Version.ValueString()), "documentServer": fmt.Sprintf("\"%s\"", config.AppConfiguration.DocumentServer.ValueString()), "logLevel": fmt.Sprintf("\"%s\"", config.AppConfiguration.LogLevel.ValueString())}), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 50, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *NextcloudResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data NextcloudModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 50, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 1055: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString()), "psqlEnabled": fmt.Sprintf("%v", data.StartupConfiguration.PsqlEnabled.ValueBool()), "redisEnabled": fmt.Sprintf("%v", data.StartupConfiguration.RedisEnabled.ValueBool()), "psqlUid": fmt.Sprintf("\"%s\"", data.StartupConfiguration.PsqlUid.ValueString()), "redisUid": fmt.Sprintf("\"%s\"", data.StartupConfiguration.RedisUid.ValueString())}), + 1056: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64())}), + 1057: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", data.AccessConfiguration.Domain.ValueString())}), + 1058: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.AppConfiguration.Version.ValueString()), "documentServer": fmt.Sprintf("\"%s\"", data.AppConfiguration.DocumentServer.ValueString()), "logLevel": fmt.Sprintf("\"%s\"", data.AppConfiguration.LogLevel.ValueString())}), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 50, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 50, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *NextcloudResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state NextcloudModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 50, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *NextcloudResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan NextcloudModel + var state NextcloudModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 1059: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64())}), + 1060: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", plan.AppConfiguration.Version.ValueString()), "documentServer": fmt.Sprintf("\"%s\"", plan.AppConfiguration.DocumentServer.ValueString()), "logLevel": fmt.Sprintf("\"%s\"", plan.AppConfiguration.LogLevel.ValueString())}), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 50, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *NextcloudResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state NextcloudModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *NextcloudResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *NextcloudResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/81_superset_resource.go b/generated/dev/go/81_superset_resource.go new file mode 100644 index 0000000..94602e6 --- /dev/null +++ b/generated/dev/go/81_superset_resource.go @@ -0,0 +1,447 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: superset +// Service ID: 81 + +var _ resource.Resource = &SupersetResource{} +var _ resource.ResourceWithModifyPlan = &SupersetResource{} +var _ resource.ResourceWithImportState = &SupersetResource{} + +type SupersetResource struct { + client *core.UniversalClient +} + +type SupersetModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + Domain types.String `tfsdk:"domain"` + Emails types.String `tfsdk:"emails"` + ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"` + ResourceDisk types.Int64 `tfsdk:"resource_disk"` + ResourceInstances types.Int64 `tfsdk:"resource_instances"` + ResourceMemory types.Int64 `tfsdk:"resource_memory"` + ResourceRealm types.String `tfsdk:"resource_realm"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewSupersetResource() resource.Resource { + return &SupersetResource{} +} + +func (r *SupersetResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_superset" +} + +func (r *SupersetResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("superset-1"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."}, + "emails": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("testuser1"), MarkdownDescription: "Учётная запись, которой будут выданы админские права. В формате email [ну не совсем. Для доменных учетных записей - без доменного суффикса]. Обязана находиться в Keycloak. Можно передавать несколько через пробел Не изменяется после создания."}, + "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Квота ядра пода (Указывается в mili). 1000 Mili = 1 ядро Не изменяется после создания."}, + "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Квота диска (Указывается в GIGAbytes) Не изменяется после создания."}, + "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Количество узлов Не изменяется после создания."}, + "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Квота памяти пода (Указывается в Megabytes) Не изменяется после создания."}, + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *SupersetResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *SupersetModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *SupersetModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan SupersetModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.Emails.IsNull() && !plan.Emails.IsUnknown() && !state.Emails.IsNull() && !state.Emails.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Emails.ValueString(), state.Emails.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить emails", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() { + if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_c_p_u", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() { + if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_disk", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() { + if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_instances", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() { + if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_memory", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.ResourceRealm.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + return + } + if config.ResourceRealm.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 14: resources_core.FormatString(config.Domain), + 20: resources_core.FormatString(config.Emails), + 96: resources_core.FormatInt64(config.ResourceCPU), + 97: resources_core.FormatInt64(config.ResourceMemory), + 98: resources_core.FormatInt64(config.ResourceDisk), + 109: resources_core.FormatString(config.ResourceRealm), + 227: resources_core.FormatInt64(config.ResourceInstances), + } + desiredDomain := "" + if !config.Domain.IsNull() && !config.Domain.IsUnknown() { + desiredDomain = config.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 81, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *SupersetResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data SupersetModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + if !data.Domain.IsNull() && !data.Domain.IsUnknown() { + desiredDomain = data.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 81, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 14: resources_core.FormatString(data.Domain), + 20: resources_core.FormatString(data.Emails), + 96: resources_core.FormatInt64(data.ResourceCPU), + 97: resources_core.FormatInt64(data.ResourceMemory), + 98: resources_core.FormatInt64(data.ResourceDisk), + 109: resources_core.FormatString(data.ResourceRealm), + 227: resources_core.FormatInt64(data.ResourceInstances), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 81, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 81, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "emails", Field: "Emails", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *SupersetResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state SupersetModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 81, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "emails", Field: "Emails", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *SupersetResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan SupersetModel + var state SupersetModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 81, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "emails", Field: "Emails", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *SupersetResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state SupersetModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *SupersetResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *SupersetResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/82_harbor_resource.go b/generated/dev/go/82_harbor_resource.go new file mode 100644 index 0000000..b53b4c3 --- /dev/null +++ b/generated/dev/go/82_harbor_resource.go @@ -0,0 +1,482 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: harbor +// Service ID: 82 + +var _ resource.Resource = &HarborResource{} +var _ resource.ResourceWithModifyPlan = &HarborResource{} +var _ resource.ResourceWithImportState = &HarborResource{} + +type HarborResource struct { + client *core.UniversalClient +} + +type HarborModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + Domain types.String `tfsdk:"domain"` + ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"` + ResourceInstances types.Int64 `tfsdk:"resource_instances"` + ResourceMemory types.Int64 `tfsdk:"resource_memory"` + ResourceRealm types.String `tfsdk:"resource_realm"` + S3Uid types.String `tfsdk:"s3_uid"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewHarborResource() resource.Resource { + return &HarborResource{} +} + +func (r *HarborResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_harbor" +} + +func (r *HarborResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("harbor-1"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."}, + "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1000), MarkdownDescription: "Квота ядра пода (Указывается в mili). 1000Mili = 1 Ядро"}, + "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Количество узлов"}, + "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1024), MarkdownDescription: "Квота памяти пода (Указывается в Megabytes)"}, + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + "s3_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UUID экземпляра услуги S3 Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *HarborResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *HarborModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *HarborModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan HarborModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan. + // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов. + // Resolve (displayName → UUID или uppercase → lowercase) нужен только для + // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка + // "Provider produced invalid plan: planned value does not match config value". + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.S3Uid.IsNull() && !plan.S3Uid.IsUnknown() && !state.S3Uid.IsNull() && !state.S3Uid.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.S3Uid.ValueString(), state.S3Uid.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить s3_uid", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.ResourceRealm.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + return + } + if config.ResourceRealm.IsUnknown() { + return + } + if config.S3Uid.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "s3_uid is required.") + return + } + if config.S3Uid.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + if !config.S3Uid.IsNull() && !config.S3Uid.IsUnknown() { + resolvedS3Uid, err := r.client.ResolveRefSvcParamValue(ctx, 12, config.S3Uid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve s3_uid", err.Error()) + } else if resolvedS3Uid != "" && resolvedS3Uid != config.S3Uid.ValueString() { + config.S3Uid = types.StringValue(resolvedS3Uid) + } + } + params := map[int]string{ + 16: resources_core.FormatString(config.Domain), + 110: resources_core.FormatString(config.ResourceRealm), + 228: resources_core.FormatInt64(config.ResourceCPU), + 229: resources_core.FormatInt64(config.ResourceMemory), + 230: resources_core.FormatInt64(config.ResourceInstances), + 231: resources_core.FormatString(config.S3Uid), + } + desiredDomain := "" + if !config.Domain.IsNull() && !config.Domain.IsUnknown() { + desiredDomain = config.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 82, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *HarborResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data HarborModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + return + } + if data.S3Uid.IsNull() || data.S3Uid.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "s3_uid is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + if !data.Domain.IsNull() && !data.Domain.IsUnknown() { + desiredDomain = data.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 82, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 16: resources_core.FormatString(data.Domain), + 110: resources_core.FormatString(data.ResourceRealm), + 228: resources_core.FormatInt64(data.ResourceCPU), + 229: resources_core.FormatInt64(data.ResourceMemory), + 230: resources_core.FormatInt64(data.ResourceInstances), + 231: resources_core.FormatString(data.S3Uid), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 82, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 82, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + // Restore user-provided casing в state (Create). + // EqualFold = «совпадают ли значения без учёта регистра?» + // Если да — значит API вернул «туже» строку, только в другом регистре. + // Заменяем state на original (то что было в plan/config пользователя). + // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint. + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *HarborResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state HarborModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 82, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + // Restore user-provided casing в state (Read). + // При чтении у нас нет plan — но предыдущий state уже хранит значение + // в регистре пользователя (после первого Create оно было восстановлено). + // Берём prior state (переменная state) как эталон регистра. + // Если API вернул то же UUID только строчными буквами — восстанавливаем. + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *HarborResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan HarborModel + var state HarborModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.ResourceInstances.IsNull() != state.ResourceInstances.IsNull() || plan.ResourceInstances.IsUnknown() != state.ResourceInstances.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() { + if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceCPU.IsNull() != state.ResourceCPU.IsNull() || plan.ResourceCPU.IsUnknown() != state.ResourceCPU.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() { + if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceMemory.IsNull() != state.ResourceMemory.IsNull() || plan.ResourceMemory.IsUnknown() != state.ResourceMemory.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() { + if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 822: resources_core.FormatInt64(plan.ResourceInstances), + 823: resources_core.FormatInt64(plan.ResourceCPU), + 824: resources_core.FormatInt64(plan.ResourceMemory), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 82, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + // Restore user-provided casing в state (Update). + // После API-вызова modify state содержит значения в lower-case от API. + // Plan == config == то что написал пользователь (регистр неизменён). + // EqualFold: если UUID совпадает без учёта регистра — берём из plan. + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *HarborResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state HarborModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *HarborResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *HarborResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/86_k8s_velero_resource.go b/generated/dev/go/86_k8s_velero_resource.go new file mode 100644 index 0000000..1a180ac --- /dev/null +++ b/generated/dev/go/86_k8s_velero_resource.go @@ -0,0 +1,401 @@ +package resources_gen + +import ( + "context" + "fmt" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: k8s_velero +// Service ID: 86 + +var _ resource.Resource = &K8sVeleroResource{} +var _ resource.ResourceWithModifyPlan = &K8sVeleroResource{} +var _ resource.ResourceWithImportState = &K8sVeleroResource{} + +type K8sVeleroResource struct { + client *core.UniversalClient +} + +// K8sVeleroBackupConfigurationModel — вложенная модель для map-fixed параметра backupConfiguration. +type K8sVeleroBackupConfigurationModel struct { + S3BucketUid types.String `tfsdk:"s3_bucket_uid" json:"s3BucketUid"` +} + +// K8sVeleroClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type K8sVeleroClusterConfigurationModel struct { + Memory types.Int64 `tfsdk:"memory" json:"memory"` + Replicas types.Int64 `tfsdk:"replicas" json:"replicas"` + Cpu types.Int64 `tfsdk:"cpu" json:"cpu"` +} + +// K8sVeleroNodeAgentConfigurationModel — вложенная модель для map-fixed параметра nodeAgentConfiguration. +type K8sVeleroNodeAgentConfigurationModel struct { + Cpu types.Int64 `tfsdk:"cpu" json:"cpu"` + Memory types.Int64 `tfsdk:"memory" json:"memory"` +} + +// K8sVeleroStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type K8sVeleroStartupConfigurationModel struct { + ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"` +} + +type K8sVeleroModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + BackupConfiguration *K8sVeleroBackupConfigurationModel `tfsdk:"backup_configuration"` + ClusterConfiguration *K8sVeleroClusterConfigurationModel `tfsdk:"cluster_configuration"` + NodeAgentConfiguration *K8sVeleroNodeAgentConfigurationModel `tfsdk:"node_agent_configuration"` + StartupConfiguration *K8sVeleroStartupConfigurationModel `tfsdk:"startup_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewK8sVeleroResource() resource.Resource { + return &K8sVeleroResource{} +} + +func (r *K8sVeleroResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_k8s_velero" +} + +func (r *K8sVeleroResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "backup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "s3_bucket_uid": schema.StringAttribute{Required: true, MarkdownDescription: "Экземпляр S3-Бакета обязателен
Учётная запись будет создана автоматически, если её нету"}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2048), MarkdownDescription: "Указывается в Megabytes"}, + "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)}, + "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1000), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"}, + }}, + "node_agent_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2000), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"}, + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2048), MarkdownDescription: "Указывается в Megabytes"}, + }}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "resource_realm": schema.StringAttribute{Required: true}, + }}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *K8sVeleroResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *K8sVeleroModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *K8sVeleroModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan K8sVeleroModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 962: resources_core.BuildJSON(map[string]string{"memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64())}), + 963: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}), + 964: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.NodeAgentConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.NodeAgentConfiguration.Memory.ValueInt64())}), + 1076: resources_core.BuildJSON(map[string]string{"s3BucketUid": fmt.Sprintf("\"%s\"", config.BackupConfiguration.S3BucketUid.ValueString())}), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 86, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *K8sVeleroResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data K8sVeleroModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 86, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 962: resources_core.BuildJSON(map[string]string{"memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64())}), + 963: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}), + 964: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.NodeAgentConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.NodeAgentConfiguration.Memory.ValueInt64())}), + 1076: resources_core.BuildJSON(map[string]string{"s3BucketUid": fmt.Sprintf("\"%s\"", data.BackupConfiguration.S3BucketUid.ValueString())}), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 86, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 86, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "nodeAgentConfiguration", Field: "NodeAgentConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *K8sVeleroResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state K8sVeleroModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 86, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "nodeAgentConfiguration", Field: "NodeAgentConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *K8sVeleroResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan K8sVeleroModel + var state K8sVeleroModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 965: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64())}), + 966: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.NodeAgentConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.NodeAgentConfiguration.Memory.ValueInt64())}), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 86, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "nodeAgentConfiguration", Field: "NodeAgentConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *K8sVeleroResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state K8sVeleroModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *K8sVeleroResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *K8sVeleroResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/87_k8svalkey_resource.go b/generated/dev/go/87_k8svalkey_resource.go new file mode 100644 index 0000000..1ac11ca --- /dev/null +++ b/generated/dev/go/87_k8svalkey_resource.go @@ -0,0 +1,423 @@ +package resources_gen + +import ( + "context" + "fmt" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: k8svalkey +// Service ID: 87 + +var _ resource.Resource = &K8svalkeyResource{} +var _ resource.ResourceWithModifyPlan = &K8svalkeyResource{} +var _ resource.ResourceWithImportState = &K8svalkeyResource{} + +type K8svalkeyResource struct { + client *core.UniversalClient +} + +// K8svalkeyAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration. +type K8svalkeyAccessConfigurationModel struct { + MasterIpSpace types.String `tfsdk:"master_ip_space" json:"masterIpSpace"` + MasterAccessList types.String `tfsdk:"master_access_list" json:"masterAccessList"` +} + +// K8svalkeyClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type K8svalkeyClusterConfigurationModel struct { + Cpu types.Int64 `tfsdk:"cpu" json:"cpu"` + Memory types.Int64 `tfsdk:"memory" json:"memory"` + Replicas types.Int64 `tfsdk:"replicas" json:"replicas"` + Shards types.Int64 `tfsdk:"shards" json:"shards"` + Disk types.Int64 `tfsdk:"disk" json:"disk"` +} + +// K8svalkeyStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type K8svalkeyStartupConfigurationModel struct { + ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"` +} + +// K8svalkeyValkeyConfigurationModel — вложенная модель для map-fixed параметра valkeyConfiguration. +type K8svalkeyValkeyConfigurationModel struct { + Version types.String `tfsdk:"version" json:"version"` +} + +type K8svalkeyModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessConfiguration *K8svalkeyAccessConfigurationModel `tfsdk:"access_configuration"` + ClusterConfiguration *K8svalkeyClusterConfigurationModel `tfsdk:"cluster_configuration"` + StartupConfiguration *K8svalkeyStartupConfigurationModel `tfsdk:"startup_configuration"` + ValkeyConf types.String `tfsdk:"valkey_conf"` + ValkeyConfiguration *K8svalkeyValkeyConfigurationModel `tfsdk:"valkey_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewK8svalkeyResource() resource.Resource { + return &K8svalkeyResource{} +} + +func (r *K8svalkeyResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_k8svalkey" +} + +func (r *K8svalkeyResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "master_ip_space": schema.StringAttribute{Required: true, MarkdownDescription: "Из какого Ip-Space резервировать внешний IP
Список предоставляется из Организации, в котором развёрнута ресурсная платформа"}, + "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"}, + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Указывается в Megabytes"}, + "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)}, + "shards": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)}, + "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Указывается в GIGAbytes"}, + }}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"}, + }}, + "valkey_conf": schema.StringAttribute{Optional: true, MarkdownDescription: "Не изменяется после создания."}, + "valkey_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("0.1.0")}, + }}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *K8svalkeyResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *K8svalkeyModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *K8svalkeyModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan K8svalkeyModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.ValkeyConf.IsNull() && !plan.ValkeyConf.IsUnknown() && !state.ValkeyConf.IsNull() && !state.ValkeyConf.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ValkeyConf.ValueString(), state.ValkeyConf.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить valkey_conf", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 1066: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}), + 1067: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "shards": fmt.Sprintf("%d", config.ClusterConfiguration.Shards.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64())}), + 1068: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterAccessList.ValueString())}), + 1071: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.ValkeyConfiguration.Version.ValueString())}), + 1072: resources_core.FormatString(config.ValkeyConf), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 87, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *K8svalkeyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data K8svalkeyModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 87, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 1066: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}), + 1067: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "shards": fmt.Sprintf("%d", data.ClusterConfiguration.Shards.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64())}), + 1068: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterAccessList.ValueString())}), + 1071: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.ValkeyConfiguration.Version.ValueString())}), + 1072: resources_core.FormatString(data.ValkeyConf), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 87, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 87, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + {Code: "valkeyConf", Field: "ValkeyConf", Type: "array-map-fixed"}, + {Code: "valkeyConfiguration", Field: "ValkeyConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *K8svalkeyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state K8svalkeyModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 87, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + {Code: "valkeyConf", Field: "ValkeyConf", Type: "array-map-fixed"}, + {Code: "valkeyConfiguration", Field: "ValkeyConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *K8svalkeyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan K8svalkeyModel + var state K8svalkeyModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 1069: resources_core.BuildJSON(map[string]string{"shards": fmt.Sprintf("%d", plan.ClusterConfiguration.Shards.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64())}), + 1070: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterAccessList.ValueString())}), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 87, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + {Code: "valkeyConf", Field: "ValkeyConf", Type: "array-map-fixed"}, + {Code: "valkeyConfiguration", Field: "ValkeyConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *K8svalkeyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state K8svalkeyModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *K8svalkeyResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *K8svalkeyResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/87_k8svalkey_user_resource.go b/generated/dev/go/87_k8svalkey_user_resource.go new file mode 100644 index 0000000..3dbe8e0 --- /dev/null +++ b/generated/dev/go/87_k8svalkey_user_resource.go @@ -0,0 +1,358 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: k8svalkey +// Subresource: user + +var _ resource.Resource = &K8svalkeyUserResource{} + +// K8svalkeyUserResource manages a subresource via operations. +type K8svalkeyUserResource struct { + client *core.UniversalClient +} + +type K8svalkeyUserModel struct { + ID types.String `tfsdk:"id"` + K8svalkeyID types.String `tfsdk:"k8svalkey_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + Permissions types.String `tfsdk:"permissions"` + Username types.String `tfsdk:"username"` +} + +func NewK8svalkeyUserResource() resource.Resource { + return &K8svalkeyUserResource{} +} + +func (r *K8svalkeyUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_k8svalkey_user" +} + +func (r *K8svalkeyUserResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "k8svalkey_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "permissions": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "username": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *K8svalkeyUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan K8svalkeyUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if plan.Permissions.IsNull() || plan.Permissions.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "permissions is required.") + return + } + if plan.Username.IsNull() || plan.Username.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "username is required.") + return + } + + instanceUID := strings.TrimSpace(plan.K8svalkeyID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "permissions": resources_core.FormatString(plan.Permissions), + "username": resources_core.FormatString(plan.Username), + } + identityCodes := []string{ + "permissions", + "username", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "username": resources_core.FormatString(plan.Username), + "permissions": resources_core.FormatString(plan.Permissions), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *K8svalkeyUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state K8svalkeyUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *K8svalkeyUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan K8svalkeyUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state K8svalkeyUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.K8svalkeyID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{ + "permissions": resources_core.FormatString(plan.Permissions), + "username": resources_core.FormatString(plan.Username), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *K8svalkeyUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state K8svalkeyUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_user" == "" { + return + } + + instanceUID := strings.TrimSpace(state.K8svalkeyID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "permissions": resources_core.FormatString(state.Permissions), + "username": resources_core.FormatString(state.Username), + } + identityCodes := []string{ + "permissions", + "username", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "username": resources_core.FormatString(state.Username), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_user", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *K8svalkeyUserResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/88_k8s_ziti_controller_resource.go b/generated/dev/go/88_k8s_ziti_controller_resource.go new file mode 100644 index 0000000..234875e --- /dev/null +++ b/generated/dev/go/88_k8s_ziti_controller_resource.go @@ -0,0 +1,431 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: k8s_ziti_controller +// Service ID: 88 + +var _ resource.Resource = &K8sZitiControllerResource{} +var _ resource.ResourceWithModifyPlan = &K8sZitiControllerResource{} +var _ resource.ResourceWithImportState = &K8sZitiControllerResource{} + +type K8sZitiControllerResource struct { + client *core.UniversalClient +} + +type K8sZitiControllerModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + Domain types.String `tfsdk:"domain"` + ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"` + ResourceDisk types.Int64 `tfsdk:"resource_disk"` + ResourceInstances types.Int64 `tfsdk:"resource_instances"` + ResourceMemory types.Int64 `tfsdk:"resource_memory"` + ResourceRealm types.String `tfsdk:"resource_realm"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewK8sZitiControllerResource() resource.Resource { + return &K8sZitiControllerResource{} +} + +func (r *K8sZitiControllerResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_k8s_ziti_controller" +} + +func (r *K8sZitiControllerResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8sZitiController-1"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."}, + "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(300), MarkdownDescription: "Квота ядра пода (milicores) Не изменяется после создания."}, + "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Квота диска (Указывается в GIGAbytes) Не изменяется после создания."}, + "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Количество узлов Не изменяется после создания."}, + "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Квота памяти пода (MBytes) Не изменяется после создания."}, + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *K8sZitiControllerResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *K8sZitiControllerModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *K8sZitiControllerModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan K8sZitiControllerModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() { + if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_c_p_u", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() { + if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_disk", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() { + if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_instances", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() { + if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_memory", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.ResourceRealm.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + return + } + if config.ResourceRealm.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 294: resources_core.FormatInt64(config.ResourceCPU), + 295: resources_core.FormatInt64(config.ResourceMemory), + 296: resources_core.FormatInt64(config.ResourceDisk), + 297: resources_core.FormatInt64(config.ResourceInstances), + 298: resources_core.FormatString(config.ResourceRealm), + 299: resources_core.FormatString(config.Domain), + } + desiredDomain := "" + if !config.Domain.IsNull() && !config.Domain.IsUnknown() { + desiredDomain = config.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 88, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *K8sZitiControllerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data K8sZitiControllerModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + if !data.Domain.IsNull() && !data.Domain.IsUnknown() { + desiredDomain = data.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 88, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 294: resources_core.FormatInt64(data.ResourceCPU), + 295: resources_core.FormatInt64(data.ResourceMemory), + 296: resources_core.FormatInt64(data.ResourceDisk), + 297: resources_core.FormatInt64(data.ResourceInstances), + 298: resources_core.FormatString(data.ResourceRealm), + 299: resources_core.FormatString(data.Domain), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 88, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 88, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *K8sZitiControllerResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state K8sZitiControllerModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 88, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *K8sZitiControllerResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan K8sZitiControllerModel + var state K8sZitiControllerModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 88, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *K8sZitiControllerResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state K8sZitiControllerModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *K8sZitiControllerResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *K8sZitiControllerResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/89_flask_resource.go b/generated/dev/go/89_flask_resource.go new file mode 100644 index 0000000..f6624df --- /dev/null +++ b/generated/dev/go/89_flask_resource.go @@ -0,0 +1,449 @@ +package resources_gen + +import ( + "context" + "fmt" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: flask +// Service ID: 89 + +var _ resource.Resource = &FlaskResource{} +var _ resource.ResourceWithModifyPlan = &FlaskResource{} +var _ resource.ResourceWithImportState = &FlaskResource{} + +type FlaskResource struct { + client *core.UniversalClient +} + +// FlaskAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration. +type FlaskAccessConfigurationModel struct { + Domain types.String `tfsdk:"domain" json:"domain"` +} + +// FlaskAppConfigurationModel — вложенная модель для map-fixed параметра appConfiguration. +type FlaskAppConfigurationModel struct { + Version types.String `tfsdk:"version" json:"version"` + GitPath types.String `tfsdk:"git_path" json:"gitPath"` + HealthPath types.String `tfsdk:"health_path" json:"healthPath"` +} + +// FlaskClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type FlaskClusterConfigurationModel struct { + Replicas types.Int64 `tfsdk:"replicas" json:"replicas"` + Cpu types.Int64 `tfsdk:"cpu" json:"cpu"` + Memory types.Int64 `tfsdk:"memory" json:"memory"` +} + +// FlaskStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type FlaskStartupConfigurationModel struct { + ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"` +} + +type FlaskModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + // --- Redeploy support (ARCHITECTURE.md) --- + // git_revision: при изменении вызывает redeploy вместо modify. + // Опциональное поле — если не задано, modify работает как обычно. + GitRevision types.String `tfsdk:"git_revision"` + AccessConfiguration *FlaskAccessConfigurationModel `tfsdk:"access_configuration"` + AppConfiguration *FlaskAppConfigurationModel `tfsdk:"app_configuration"` + ClusterConfiguration *FlaskClusterConfigurationModel `tfsdk:"cluster_configuration"` + JsonEnv types.String `tfsdk:"json_env"` + StartupConfiguration *FlaskStartupConfigurationModel `tfsdk:"startup_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewFlaskResource() resource.Resource { + return &FlaskResource{} +} + +func (r *FlaskResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_flask" +} + +func (r *FlaskResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("flask-y0lfavjf"), MarkdownDescription: "Если указан FQDN в формате: `myapp.example.com`, предварительно будет проверяться DNS-запись (А-запись **должна смотреть на IP Ingress кластера ресурсной платформы**)

Если указан в формате `myapp`, DNS-запись будет автоматически создана в служебной DNS-зоне"}, + }}, + "app_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("3.12")}, + "git_path": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("https://github.com/Foxyhhd/Baldurs-Gate-test.git"), MarkdownDescription: "Путь до репозитория приложения
Обязательно должен быть передан как URL: `https://....`

Должен быть сетевой доступ до URL для скачивания исходников"}, + "health_path": schema.StringAttribute{Optional: true, MarkdownDescription: "Путь до healtcheck для запуска пода в Kubernetes, который отдаст 200 код
Пустое значение означает, что будет проверяться исключительно доступность приложения по порту"}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)}, + "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(200), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"}, + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(256), MarkdownDescription: "Указывается в Megabytes"}, + }}, + "json_env": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("{}")}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"}, + }}, + // --- Redeploy support (ARCHITECTURE.md) --- + // При изменении git_revision вызывается redeploy вместо modify. + // Если не задано — modify работает без изменений. + "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *FlaskResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *FlaskModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *FlaskModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan FlaskModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 1037: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}), + 1038: resources_core.BuildJSON(map[string]string{"replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64())}), + 1039: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", config.AccessConfiguration.Domain.ValueString())}), + 1040: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", config.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", config.AppConfiguration.HealthPath.ValueString())}), + 1041: resources_core.FormatString(config.JsonEnv), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 89, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *FlaskResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data FlaskModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 89, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 1037: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}), + 1038: resources_core.BuildJSON(map[string]string{"replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64())}), + 1039: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", data.AccessConfiguration.Domain.ValueString())}), + 1040: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", data.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", data.AppConfiguration.HealthPath.ValueString())}), + 1041: resources_core.FormatString(data.JsonEnv), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 89, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 89, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "jsonEnv", Field: "JsonEnv", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *FlaskResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state FlaskModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 89, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "jsonEnv", Field: "JsonEnv", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *FlaskResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan FlaskModel + var state FlaskModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.JsonEnv.IsNull() != state.JsonEnv.IsNull() || plan.JsonEnv.IsUnknown() != state.JsonEnv.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.JsonEnv.IsNull() && !plan.JsonEnv.IsUnknown() && !state.JsonEnv.IsNull() && !state.JsonEnv.IsUnknown() { + if !strings.EqualFold(plan.JsonEnv.ValueString(), state.JsonEnv.ValueString()) { + hasServiceParamChanges = true + } + } + } + // --- Redeploy support (ARCHITECTURE.md) --- + // Проверяем изменился ли git_revision. + // Если да — вызываем redeploy вместо modify. + // git_revision = "" (не задано) → обычный modify. + redeployRequested := false + if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() { + if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() { + redeployRequested = true + } + } + + if !hasServiceParamChanges && !redeployRequested { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + // --- Redeploy support (ARCHITECTURE.md) --- + if redeployRequested { + // Вызываем redeploy — пустой params (операция не требует CFS-параметров) + if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + } else { + params := map[int]string{ + 1042: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64())}), + 1043: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", plan.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", plan.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", plan.AppConfiguration.HealthPath.ValueString())}), + 1044: resources_core.FormatString(plan.JsonEnv), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 89, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "jsonEnv", Field: "JsonEnv", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *FlaskResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state FlaskModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *FlaskResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *FlaskResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/90_postgres_backup_resource.go b/generated/dev/go/90_postgres_backup_resource.go new file mode 100644 index 0000000..957eb3e --- /dev/null +++ b/generated/dev/go/90_postgres_backup_resource.go @@ -0,0 +1,326 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: postgres +// Subresource: backup + +var _ resource.Resource = &PostgresBackupResource{} + +// PostgresBackupResource manages a subresource via operations. +type PostgresBackupResource struct { + client *core.UniversalClient +} + +type PostgresBackupModel struct { + ID types.String `tfsdk:"id"` + PostgresID types.String `tfsdk:"postgres_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` +} + +func NewPostgresBackupResource() resource.Resource { + return &PostgresBackupResource{} +} + +func (r *PostgresBackupResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_postgres_backup" +} + +func (r *PostgresBackupResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "postgres_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *PostgresBackupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan PostgresBackupModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.PostgresID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{} + identityCodes := []string{} + listKey, idKey := resources_core.ResolveSubresourceStateKeys("backup", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{}) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_backup", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_backup", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *PostgresBackupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state PostgresBackupModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *PostgresBackupResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan PostgresBackupModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state PostgresBackupModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.PostgresID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{} + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *PostgresBackupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state PostgresBackupModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + return + } + + instanceUID := strings.TrimSpace(state.PostgresID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{} + identityCodes := []string{} + listKey, idKey := resources_core.ResolveSubresourceStateKeys("backup", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{}) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *PostgresBackupResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/90_postgres_database_resource.go b/generated/dev/go/90_postgres_database_resource.go new file mode 100644 index 0000000..7310720 --- /dev/null +++ b/generated/dev/go/90_postgres_database_resource.go @@ -0,0 +1,358 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: postgres +// Subresource: database + +var _ resource.Resource = &PostgresDatabaseResource{} + +// PostgresDatabaseResource manages a subresource via operations. +type PostgresDatabaseResource struct { + client *core.UniversalClient +} + +type PostgresDatabaseModel struct { + ID types.String `tfsdk:"id"` + PostgresID types.String `tfsdk:"postgres_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + DbName types.String `tfsdk:"db_name"` + DbOwner types.String `tfsdk:"db_owner"` +} + +func NewPostgresDatabaseResource() resource.Resource { + return &PostgresDatabaseResource{} +} + +func (r *PostgresDatabaseResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_postgres_database" +} + +func (r *PostgresDatabaseResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "postgres_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "db_name": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "db_owner": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *PostgresDatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan PostgresDatabaseModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if plan.DbName.IsNull() || plan.DbName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "db_name is required.") + return + } + if plan.DbOwner.IsNull() || plan.DbOwner.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "db_owner is required.") + return + } + + instanceUID := strings.TrimSpace(plan.PostgresID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "dbName": resources_core.FormatString(plan.DbName), + "dbOwner": resources_core.FormatString(plan.DbOwner), + } + identityCodes := []string{ + "dbName", + "dbOwner", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("database", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "dbName": resources_core.FormatString(plan.DbName), + "dbOwner": resources_core.FormatString(plan.DbOwner), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_database", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_database", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *PostgresDatabaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state PostgresDatabaseModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *PostgresDatabaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan PostgresDatabaseModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state PostgresDatabaseModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.PostgresID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{ + "dbName": resources_core.FormatString(plan.DbName), + "dbOwner": resources_core.FormatString(plan.DbOwner), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *PostgresDatabaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state PostgresDatabaseModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_database" == "" { + return + } + + instanceUID := strings.TrimSpace(state.PostgresID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "dbName": resources_core.FormatString(state.DbName), + "dbOwner": resources_core.FormatString(state.DbOwner), + } + identityCodes := []string{ + "dbName", + "dbOwner", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("database", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "dbName": resources_core.FormatString(state.DbName), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_database", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *PostgresDatabaseResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/90_postgres_resource.go b/generated/dev/go/90_postgres_resource.go new file mode 100644 index 0000000..518f22c --- /dev/null +++ b/generated/dev/go/90_postgres_resource.go @@ -0,0 +1,499 @@ +package resources_gen + +import ( + "context" + "fmt" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: postgres +// Service ID: 90 + +var _ resource.Resource = &PostgresResource{} +var _ resource.ResourceWithModifyPlan = &PostgresResource{} +var _ resource.ResourceWithImportState = &PostgresResource{} + +type PostgresResource struct { + client *core.UniversalClient +} + +// PostgresAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration. +type PostgresAccessConfigurationModel struct { + MasterIpSpace types.String `tfsdk:"master_ip_space" json:"masterIpSpace"` + MasterAccessList types.String `tfsdk:"master_access_list" json:"masterAccessList"` + SlaveIpSpace types.String `tfsdk:"slave_ip_space" json:"slaveIpSpace"` + SlaveAccessList types.String `tfsdk:"slave_access_list" json:"slaveAccessList"` +} + +// PostgresAutoscaleConfigurationModel — вложенная модель для map-fixed параметра autoscaleConfiguration. +type PostgresAutoscaleConfigurationModel struct { + Percent types.Int64 `tfsdk:"percent" json:"percent"` + Enabled types.Bool `tfsdk:"enabled" json:"enabled"` + Schedule types.Int64 `tfsdk:"schedule" json:"schedule"` + Quota types.Int64 `tfsdk:"quota" json:"quota"` +} + +// PostgresBackupConfigurationModel — вложенная модель для map-fixed параметра backupConfiguration. +type PostgresBackupConfigurationModel struct { + S3Uid types.String `tfsdk:"s3_uid" json:"s3Uid"` + Retain types.Int64 `tfsdk:"retain" json:"retain"` + Schedule types.String `tfsdk:"schedule" json:"schedule"` +} + +// PostgresClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type PostgresClusterConfigurationModel struct { + Memory types.Int64 `tfsdk:"memory" json:"memory"` + Replicas types.Int64 `tfsdk:"replicas" json:"replicas"` + Disk types.Int64 `tfsdk:"disk" json:"disk"` + Cpu types.Int64 `tfsdk:"cpu" json:"cpu"` +} + +// PostgresMtlsConfigurationModel — вложенная модель для map-fixed параметра mtlsConfiguration. +type PostgresMtlsConfigurationModel struct { + Type types.String `tfsdk:"type" json:"type"` + DurationCA types.Int64 `tfsdk:"duration_c_a" json:"durationCA"` + DurationServer types.Int64 `tfsdk:"duration_server" json:"durationServer"` + CertCA types.String `tfsdk:"cert_c_a" json:"certCA"` + CertServer types.String `tfsdk:"cert_server" json:"certServer"` +} + +// PostgresPostgresConfigurationModel — вложенная модель для map-fixed параметра postgresConfiguration. +type PostgresPostgresConfigurationModel struct { + Version types.String `tfsdk:"version" json:"version"` + SslRequired types.Bool `tfsdk:"ssl_required" json:"sslRequired"` + PoolerMaster types.Bool `tfsdk:"pooler_master" json:"poolerMaster"` + PoolerSlave types.Bool `tfsdk:"pooler_slave" json:"poolerSlave"` +} + +// PostgresStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type PostgresStartupConfigurationModel struct { + ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"` +} + +type PostgresModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessConfiguration *PostgresAccessConfigurationModel `tfsdk:"access_configuration"` + AutoscaleConfiguration *PostgresAutoscaleConfigurationModel `tfsdk:"autoscale_configuration"` + BackupConfiguration *PostgresBackupConfigurationModel `tfsdk:"backup_configuration"` + ClusterConfiguration *PostgresClusterConfigurationModel `tfsdk:"cluster_configuration"` + MtlsConfiguration *PostgresMtlsConfigurationModel `tfsdk:"mtls_configuration"` + PostgresConf types.String `tfsdk:"postgres_conf"` + PostgresConfiguration *PostgresPostgresConfigurationModel `tfsdk:"postgres_configuration"` + StartupConfiguration *PostgresStartupConfigurationModel `tfsdk:"startup_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewPostgresResource() resource.Resource { + return &PostgresResource{} +} + +func (r *PostgresResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_postgres" +} + +func (r *PostgresResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "master_ip_space": schema.StringAttribute{Required: true, MarkdownDescription: "Из какого Ip-Space резервировать внешний IP
Список предоставляется из Организации, в котором развёрнута ресурсная платформа"}, + "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"}, + "slave_ip_space": schema.StringAttribute{Required: true, MarkdownDescription: "Из какого Ip-Space резервировать внешний IP
Список предоставляется из Организации, в котором развёрнута ресурсная платформа"}, + "slave_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"}, + }}, + "autoscale_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "percent": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Требует включения autoScale. Позволяет указать в процентах расширение от текущего макс объема
Минимальный размер на который расширяется - **1Gb**"}, + "enabled": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включает автоскейлинг PV. При включении параметра необходимо настроить autoScalePercentage и autoScaleTechWindow. Алерт при срабатывании вызывает modify у текущей услуги"}, + "schedule": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(0), MarkdownDescription: "Требует включения autoScale. Окно обновления (пока не реализовано). Указывается час в виде Integer (0 / 5 / 12 / 23)"}, + "quota": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100), MarkdownDescription: "До какого размера возможно увеличивать диск, когда включен Autoscale"}, + }}, + "backup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "s3_uid": schema.StringAttribute{Required: true, MarkdownDescription: "Экземпляр S3 обязателен"}, + "retain": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(7)}, + "schedule": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("0 0 * * *")}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Указывается в Megabytes"}, + "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)}, + "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Указывается в GIGAbytes"}, + "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"}, + }}, + "mtls_configuration": schema.SingleNestedAttribute{Optional: true, Computed: true, + Attributes: map[string]schema.Attribute{ + "type": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("off"), MarkdownDescription: "`off` - Настройка MTLS производиться не будет
`manual` - После создания кластера в полях данных будет запрос на создание сертификата. **Вам необходимо будет выписать сертификат через вашу CA** и уже после включить через операцию **modify** режим MTLS
`auto` - Сертификаты будут созданы через Cert-Manager

Режимы **auto** и **manual** требует включить настройку `Включить sslMode` - **true**
MTLS в режиме **auto** будет работать сразу, в режиме **manual** только после того как будет предоставлен **CA** и **Сертификат сервера**"}, + "duration_c_a": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(175200), MarkdownDescription: "Указывается в **часах**
Необходимо настраивать, когда параметр `Режим настройки` - **auto**"}, + "duration_server": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(87600), MarkdownDescription: "Указывается в **часах**
Не может быть больше чем время жизни СА
Обновляется **раз в год**, если выставлено значение **более 2 лет**
Обновляется **раз в пол года**, если выставлено значение **менее 2 лет**
Необходимо настраивать, когда параметр `Режим настройки` - **auto**"}, + "cert_c_a": schema.StringAttribute{Optional: true}, + "cert_server": schema.StringAttribute{Optional: true}, + }}, + "postgres_conf": schema.StringAttribute{Optional: true}, + "postgres_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("17")}, + "ssl_required": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "По умолчанию подключение обязательное (sslmode=require)"}, + "pooler_master": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включает pgPooler (на запись). Будет создан новый под
Размер пода **200m/400Mi**"}, + "pooler_slave": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включает pgPooler (на чтение). Будет создан новый под
Размер пода **200m/400Mi**"}, + }}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"}, + }}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *PostgresResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *PostgresModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *PostgresModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan PostgresModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 788: resources_core.BuildJSON(map[string]string{"memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64())}), + 789: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}), + 790: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.SlaveIpSpace.ValueString()), "slaveAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.SlaveAccessList.ValueString())}), + 791: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.PostgresConfiguration.Version.ValueString()), "sslRequired": fmt.Sprintf("%v", config.PostgresConfiguration.SslRequired.ValueBool()), "poolerMaster": fmt.Sprintf("%v", config.PostgresConfiguration.PoolerMaster.ValueBool()), "poolerSlave": fmt.Sprintf("%v", config.PostgresConfiguration.PoolerSlave.ValueBool())}), + 792: resources_core.FormatString(config.PostgresConf), + 793: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", config.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", config.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", config.BackupConfiguration.Schedule.ValueString())}), + 794: resources_core.BuildJSON(map[string]string{"percent": fmt.Sprintf("%d", config.AutoscaleConfiguration.Percent.ValueInt64()), "enabled": fmt.Sprintf("%v", config.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", config.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", config.AutoscaleConfiguration.Quota.ValueInt64())}), + 1094: resources_core.BuildJSON(map[string]string{"type": fmt.Sprintf("\"%s\"", config.MtlsConfiguration.Type.ValueString()), "durationCA": fmt.Sprintf("%d", config.MtlsConfiguration.DurationCA.ValueInt64()), "durationServer": fmt.Sprintf("%d", config.MtlsConfiguration.DurationServer.ValueInt64()), "certCA": fmt.Sprintf("\"%s\"", config.MtlsConfiguration.CertCA.ValueString()), "certServer": fmt.Sprintf("\"%s\"", config.MtlsConfiguration.CertServer.ValueString())}), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 90, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *PostgresResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data PostgresModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 90, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 788: resources_core.BuildJSON(map[string]string{"memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64())}), + 789: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}), + 790: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.SlaveIpSpace.ValueString()), "slaveAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.SlaveAccessList.ValueString())}), + 791: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.PostgresConfiguration.Version.ValueString()), "sslRequired": fmt.Sprintf("%v", data.PostgresConfiguration.SslRequired.ValueBool()), "poolerMaster": fmt.Sprintf("%v", data.PostgresConfiguration.PoolerMaster.ValueBool()), "poolerSlave": fmt.Sprintf("%v", data.PostgresConfiguration.PoolerSlave.ValueBool())}), + 792: resources_core.FormatString(data.PostgresConf), + 793: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", data.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", data.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", data.BackupConfiguration.Schedule.ValueString())}), + 794: resources_core.BuildJSON(map[string]string{"percent": fmt.Sprintf("%d", data.AutoscaleConfiguration.Percent.ValueInt64()), "enabled": fmt.Sprintf("%v", data.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", data.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", data.AutoscaleConfiguration.Quota.ValueInt64())}), + 1094: resources_core.BuildJSON(map[string]string{"type": fmt.Sprintf("\"%s\"", data.MtlsConfiguration.Type.ValueString()), "durationCA": fmt.Sprintf("%d", data.MtlsConfiguration.DurationCA.ValueInt64()), "durationServer": fmt.Sprintf("%d", data.MtlsConfiguration.DurationServer.ValueInt64()), "certCA": fmt.Sprintf("\"%s\"", data.MtlsConfiguration.CertCA.ValueString()), "certServer": fmt.Sprintf("\"%s\"", data.MtlsConfiguration.CertServer.ValueString())}), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 90, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 90, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"}, + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "mtlsConfiguration", Field: "MtlsConfiguration", Type: "map-fixed"}, + {Code: "postgresConf", Field: "PostgresConf", Type: "array-map-fixed"}, + {Code: "postgresConfiguration", Field: "PostgresConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *PostgresResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state PostgresModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 90, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"}, + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "mtlsConfiguration", Field: "MtlsConfiguration", Type: "map-fixed"}, + {Code: "postgresConf", Field: "PostgresConf", Type: "array-map-fixed"}, + {Code: "postgresConfiguration", Field: "PostgresConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *PostgresResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan PostgresModel + var state PostgresModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.PostgresConf.IsNull() != state.PostgresConf.IsNull() || plan.PostgresConf.IsUnknown() != state.PostgresConf.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.PostgresConf.IsNull() && !plan.PostgresConf.IsUnknown() && !state.PostgresConf.IsNull() && !state.PostgresConf.IsUnknown() { + if !strings.EqualFold(plan.PostgresConf.ValueString(), state.PostgresConf.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 795: resources_core.BuildJSON(map[string]string{"replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64())}), + 796: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.SlaveIpSpace.ValueString()), "slaveAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.SlaveAccessList.ValueString())}), + 797: resources_core.BuildJSON(map[string]string{"poolerSlave": fmt.Sprintf("%v", plan.PostgresConfiguration.PoolerSlave.ValueBool()), "version": fmt.Sprintf("\"%s\"", plan.PostgresConfiguration.Version.ValueString()), "sslRequired": fmt.Sprintf("%v", plan.PostgresConfiguration.SslRequired.ValueBool()), "poolerMaster": fmt.Sprintf("%v", plan.PostgresConfiguration.PoolerMaster.ValueBool())}), + 798: resources_core.FormatString(plan.PostgresConf), + 799: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", plan.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", plan.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", plan.BackupConfiguration.Schedule.ValueString())}), + 800: resources_core.BuildJSON(map[string]string{"enabled": fmt.Sprintf("%v", plan.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Percent.ValueInt64())}), + 1095: resources_core.BuildJSON(map[string]string{"certServer": fmt.Sprintf("\"%s\"", plan.MtlsConfiguration.CertServer.ValueString()), "type": fmt.Sprintf("\"%s\"", plan.MtlsConfiguration.Type.ValueString()), "durationCA": fmt.Sprintf("%d", plan.MtlsConfiguration.DurationCA.ValueInt64()), "durationServer": fmt.Sprintf("%d", plan.MtlsConfiguration.DurationServer.ValueInt64()), "certCA": fmt.Sprintf("\"%s\"", plan.MtlsConfiguration.CertCA.ValueString())}), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 90, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"}, + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "mtlsConfiguration", Field: "MtlsConfiguration", Type: "map-fixed"}, + {Code: "postgresConf", Field: "PostgresConf", Type: "array-map-fixed"}, + {Code: "postgresConfiguration", Field: "PostgresConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *PostgresResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state PostgresModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *PostgresResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *PostgresResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/90_postgres_user_resource.go b/generated/dev/go/90_postgres_user_resource.go new file mode 100644 index 0000000..8ac874c --- /dev/null +++ b/generated/dev/go/90_postgres_user_resource.go @@ -0,0 +1,367 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: postgres +// Subresource: user + +var _ resource.Resource = &PostgresUserResource{} + +// PostgresUserResource manages a subresource via operations. +type PostgresUserResource struct { + client *core.UniversalClient +} + +type PostgresUserModel struct { + ID types.String `tfsdk:"id"` + PostgresID types.String `tfsdk:"postgres_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + MtlsAccess types.Bool `tfsdk:"mtls_access"` + Role types.String `tfsdk:"role"` + Username types.String `tfsdk:"username"` +} + +func NewPostgresUserResource() resource.Resource { + return &PostgresUserResource{} +} + +func (r *PostgresUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_postgres_user" +} + +func (r *PostgresUserResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "postgres_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "mtls_access": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Подключение пользователя без ввода паролей
Политика применяется **ко всем базам** внутри Postgresql

RBAC на базы настраивать вручную Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.Bool{boolplanmodifier.RequiresReplace()}}, + "role": schema.StringAttribute{Required: true, MarkdownDescription: "Выбрать роль из списка Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "username": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *PostgresUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan PostgresUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if plan.Role.IsNull() || plan.Role.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "role is required.") + return + } + if plan.Username.IsNull() || plan.Username.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "username is required.") + return + } + + instanceUID := strings.TrimSpace(plan.PostgresID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "mtlsAccess": resources_core.FormatBool(plan.MtlsAccess), + "role": resources_core.FormatString(plan.Role), + "username": resources_core.FormatString(plan.Username), + } + identityCodes := []string{ + "mtlsAccess", + "role", + "username", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "username": resources_core.FormatString(plan.Username), + "role": resources_core.FormatString(plan.Role), + "mtlsAccess": resources_core.FormatBool(plan.MtlsAccess), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *PostgresUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state PostgresUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *PostgresUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan PostgresUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state PostgresUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.PostgresID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{ + "mtlsAccess": resources_core.FormatBool(plan.MtlsAccess), + "role": resources_core.FormatString(plan.Role), + "username": resources_core.FormatString(plan.Username), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *PostgresUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state PostgresUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_user" == "" { + return + } + + instanceUID := strings.TrimSpace(state.PostgresID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "mtlsAccess": resources_core.FormatBool(state.MtlsAccess), + "role": resources_core.FormatString(state.Role), + "username": resources_core.FormatString(state.Username), + } + identityCodes := []string{ + "mtlsAccess", + "role", + "username", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "username": resources_core.FormatString(state.Username), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_user", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *PostgresUserResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/91_redis_resource.go b/generated/dev/go/91_redis_resource.go new file mode 100644 index 0000000..2462093 --- /dev/null +++ b/generated/dev/go/91_redis_resource.go @@ -0,0 +1,393 @@ +package resources_gen + +import ( + "context" + "fmt" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: redis +// Service ID: 91 + +var _ resource.Resource = &RedisResource{} +var _ resource.ResourceWithModifyPlan = &RedisResource{} +var _ resource.ResourceWithImportState = &RedisResource{} + +type RedisResource struct { + client *core.UniversalClient +} + +// RedisAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration. +type RedisAccessConfigurationModel struct { + MasterIpSpace types.String `tfsdk:"master_ip_space" json:"masterIpSpace"` + MasterAccessList types.String `tfsdk:"master_access_list" json:"masterAccessList"` + SlaveIpSpace types.String `tfsdk:"slave_ip_space" json:"slaveIpSpace"` + SlaveAccessList types.String `tfsdk:"slave_access_list" json:"slaveAccessList"` +} + +// RedisClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type RedisClusterConfigurationModel struct { + Cpu types.Int64 `tfsdk:"cpu" json:"cpu"` + Memory types.Int64 `tfsdk:"memory" json:"memory"` + Replicas types.Int64 `tfsdk:"replicas" json:"replicas"` + Disk types.Int64 `tfsdk:"disk" json:"disk"` +} + +// RedisStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type RedisStartupConfigurationModel struct { + ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"` +} + +type RedisModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessConfiguration *RedisAccessConfigurationModel `tfsdk:"access_configuration"` + ClusterConfiguration *RedisClusterConfigurationModel `tfsdk:"cluster_configuration"` + StartupConfiguration *RedisStartupConfigurationModel `tfsdk:"startup_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewRedisResource() resource.Resource { + return &RedisResource{} +} + +func (r *RedisResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_redis" +} + +func (r *RedisResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "master_ip_space": schema.StringAttribute{Required: true, MarkdownDescription: "Из какого Ip-Space резервировать внешний IP
Список предоставляется из Организации, в котором развёрнута ресурсная платформа"}, + "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"}, + "slave_ip_space": schema.StringAttribute{Required: true, MarkdownDescription: "Из какого Ip-Space резервировать внешний IP
Список предоставляется из Организации, в котором развёрнута ресурсная платформа"}, + "slave_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"}, + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Указывается в Megabytes"}, + "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)}, + "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Указывается в GIGAbytes"}, + }}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"}, + }}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *RedisResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *RedisModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *RedisModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan RedisModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 1061: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}), + 1062: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64())}), + 1063: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.SlaveIpSpace.ValueString()), "slaveAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.SlaveAccessList.ValueString())}), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 91, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *RedisResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data RedisModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 91, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 1061: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}), + 1062: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64())}), + 1063: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.SlaveIpSpace.ValueString()), "slaveAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.SlaveAccessList.ValueString())}), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 91, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 91, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *RedisResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state RedisModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 91, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *RedisResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan RedisModel + var state RedisModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 1064: resources_core.BuildJSON(map[string]string{"replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64())}), + 1065: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.SlaveIpSpace.ValueString()), "slaveAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.SlaveAccessList.ValueString())}), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 91, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *RedisResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state RedisModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *RedisResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *RedisResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/92_mongodb_resource.go b/generated/dev/go/92_mongodb_resource.go new file mode 100644 index 0000000..2a8e46d --- /dev/null +++ b/generated/dev/go/92_mongodb_resource.go @@ -0,0 +1,389 @@ +package resources_gen + +import ( + "context" + "fmt" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: mongodb +// Service ID: 92 + +var _ resource.Resource = &MongodbResource{} +var _ resource.ResourceWithModifyPlan = &MongodbResource{} +var _ resource.ResourceWithImportState = &MongodbResource{} + +type MongodbResource struct { + client *core.UniversalClient +} + +// MongodbAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration. +type MongodbAccessConfigurationModel struct { + MasterAccessList types.String `tfsdk:"master_access_list" json:"masterAccessList"` + MasterIpSpace types.String `tfsdk:"master_ip_space" json:"masterIpSpace"` +} + +// MongodbClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type MongodbClusterConfigurationModel struct { + Memory types.Int64 `tfsdk:"memory" json:"memory"` + Replicas types.Int64 `tfsdk:"replicas" json:"replicas"` + Disk types.Int64 `tfsdk:"disk" json:"disk"` + Cpu types.Int64 `tfsdk:"cpu" json:"cpu"` +} + +// MongodbStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type MongodbStartupConfigurationModel struct { + ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"` +} + +type MongodbModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessConfiguration *MongodbAccessConfigurationModel `tfsdk:"access_configuration"` + ClusterConfiguration *MongodbClusterConfigurationModel `tfsdk:"cluster_configuration"` + StartupConfiguration *MongodbStartupConfigurationModel `tfsdk:"startup_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewMongodbResource() resource.Resource { + return &MongodbResource{} +} + +func (r *MongodbResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_mongodb" +} + +func (r *MongodbResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"}, + "master_ip_space": schema.StringAttribute{Required: true, MarkdownDescription: "Из какого Ip-Space резервировать внешний IP
Список предоставляется из Организации, в котором развёрнута ресурсная платформа"}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1024)}, + "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)}, + "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(5), MarkdownDescription: "Указывается в Gb"}, + "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1000)}, + }}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"}, + }}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *MongodbResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *MongodbModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *MongodbModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan MongodbModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 1089: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}), + 1090: resources_core.BuildJSON(map[string]string{"memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64())}), + 1091: resources_core.BuildJSON(map[string]string{"masterAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterAccessList.ValueString()), "masterIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterIpSpace.ValueString())}), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 92, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *MongodbResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data MongodbModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 92, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 1089: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}), + 1090: resources_core.BuildJSON(map[string]string{"memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64())}), + 1091: resources_core.BuildJSON(map[string]string{"masterAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterAccessList.ValueString()), "masterIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterIpSpace.ValueString())}), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 92, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 92, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *MongodbResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state MongodbModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 92, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *MongodbResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan MongodbModel + var state MongodbModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 1092: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64())}), + 1093: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterAccessList.ValueString())}), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 92, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *MongodbResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state MongodbModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *MongodbResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *MongodbResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/92_mongodb_user_resource.go b/generated/dev/go/92_mongodb_user_resource.go new file mode 100644 index 0000000..6f0e40d --- /dev/null +++ b/generated/dev/go/92_mongodb_user_resource.go @@ -0,0 +1,364 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: mongodb +// Subresource: user + +var _ resource.Resource = &MongodbUserResource{} + +// MongodbUserResource manages a subresource via operations. +type MongodbUserResource struct { + client *core.UniversalClient +} + +type MongodbUserModel struct { + ID types.String `tfsdk:"id"` + MongodbID types.String `tfsdk:"mongodb_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + DbName types.String `tfsdk:"db_name"` + Role types.String `tfsdk:"role"` + Username types.String `tfsdk:"username"` +} + +func NewMongodbUserResource() resource.Resource { + return &MongodbUserResource{} +} + +func (r *MongodbUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_mongodb_user" +} + +func (r *MongodbUserResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "mongodb_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "db_name": schema.StringAttribute{Required: true}, + "role": schema.StringAttribute{Required: true}, + "username": schema.StringAttribute{Required: true, MarkdownDescription: "Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *MongodbUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan MongodbUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if plan.DbName.IsNull() || plan.DbName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "db_name is required.") + return + } + if plan.Role.IsNull() || plan.Role.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "role is required.") + return + } + if plan.Username.IsNull() || plan.Username.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "username is required.") + return + } + + instanceUID := strings.TrimSpace(plan.MongodbID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "username": resources_core.FormatString(plan.Username), + } + identityCodes := []string{ + "username", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "username": resources_core.FormatString(plan.Username), + "dbName": resources_core.FormatString(plan.DbName), + "role": resources_core.FormatString(plan.Role), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *MongodbUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state MongodbUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *MongodbUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan MongodbUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "modify_user" == "" { + var state MongodbUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.MongodbID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{ + "username": resources_core.FormatString(plan.Username), + "dbName": resources_core.FormatString(plan.DbName), + "role": resources_core.FormatString(plan.Role), + }) + + idParams := map[string]string{ + "username": resources_core.FormatString(plan.Username), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "modify_user", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *MongodbUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state MongodbUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_user" == "" { + return + } + + instanceUID := strings.TrimSpace(state.MongodbID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "username": resources_core.FormatString(state.Username), + } + identityCodes := []string{ + "username", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "username": resources_core.FormatString(state.Username), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_user", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *MongodbUserResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/93_rabbitmq_resource.go b/generated/dev/go/93_rabbitmq_resource.go new file mode 100644 index 0000000..a43cd4d --- /dev/null +++ b/generated/dev/go/93_rabbitmq_resource.go @@ -0,0 +1,474 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: rabbitmq +// Service ID: 93 + +var _ resource.Resource = &RabbitmqResource{} +var _ resource.ResourceWithModifyPlan = &RabbitmqResource{} +var _ resource.ResourceWithImportState = &RabbitmqResource{} + +type RabbitmqResource struct { + client *core.UniversalClient +} + +type RabbitmqModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + IpSpaceNameMaster types.String `tfsdk:"ip_space_name_master"` + MasterAccessList types.String `tfsdk:"master_access_list"` + ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"` + ResourceDisk types.Int64 `tfsdk:"resource_disk"` + ResourceInstances types.Int64 `tfsdk:"resource_instances"` + ResourceMemory types.Int64 `tfsdk:"resource_memory"` + ResourceRealm types.String `tfsdk:"resource_realm"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewRabbitmqResource() resource.Resource { + return &RabbitmqResource{} +} + +func (r *RabbitmqResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_rabbitmq" +} + +func (r *RabbitmqResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "ip_space_name_master": schema.StringAttribute{Required: true, MarkdownDescription: "Пример: `internet-no-antiddos-v1`"}, + "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Список IP-адресов, с которых разрешён доступ. Если оставить пустым — доступ открыт всем.", PlanModifiers: []planmodifier.String{resources_core.JsonNormalize()}}, + "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1000), MarkdownDescription: "Пример: `2000`"}, + "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(30), MarkdownDescription: "Пример: `20`"}, + "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Пример: `1`"}, + "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1124), MarkdownDescription: "Пример: `2048`"}, + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Выбрать платформу из списка Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *RabbitmqResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *RabbitmqModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *RabbitmqModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan RabbitmqModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.IpSpaceNameMaster.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "ip_space_name_master is required.") + return + } + if config.IpSpaceNameMaster.IsUnknown() { + return + } + if config.ResourceRealm.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + return + } + if config.ResourceRealm.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 116: resources_core.FormatInt64(config.ResourceCPU), + 117: resources_core.FormatInt64(config.ResourceMemory), + 118: resources_core.FormatInt64(config.ResourceDisk), + 119: resources_core.FormatString(config.ResourceRealm), + 224: resources_core.FormatInt64(config.ResourceInstances), + 225: resources_core.FormatString(config.MasterAccessList), + 543: resources_core.FormatString(config.IpSpaceNameMaster), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 93, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *RabbitmqResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data RabbitmqModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.IpSpaceNameMaster.IsNull() || data.IpSpaceNameMaster.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "ip_space_name_master is required.") + return + } + if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 93, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 116: resources_core.FormatInt64(data.ResourceCPU), + 117: resources_core.FormatInt64(data.ResourceMemory), + 118: resources_core.FormatInt64(data.ResourceDisk), + 119: resources_core.FormatString(data.ResourceRealm), + 224: resources_core.FormatInt64(data.ResourceInstances), + 225: resources_core.FormatString(data.MasterAccessList), + 543: resources_core.FormatString(data.IpSpaceNameMaster), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 93, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 93, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "ipSpaceNameMaster", Field: "IpSpaceNameMaster", Type: "string"}, + {Code: "masterAccessList", Field: "MasterAccessList", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *RabbitmqResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state RabbitmqModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 93, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "ipSpaceNameMaster", Field: "IpSpaceNameMaster", Type: "string"}, + {Code: "masterAccessList", Field: "MasterAccessList", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *RabbitmqResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan RabbitmqModel + var state RabbitmqModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.ResourceCPU.IsNull() != state.ResourceCPU.IsNull() || plan.ResourceCPU.IsUnknown() != state.ResourceCPU.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() { + if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceMemory.IsNull() != state.ResourceMemory.IsNull() || plan.ResourceMemory.IsUnknown() != state.ResourceMemory.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() { + if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceDisk.IsNull() != state.ResourceDisk.IsNull() || plan.ResourceDisk.IsUnknown() != state.ResourceDisk.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() { + if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceInstances.IsNull() != state.ResourceInstances.IsNull() || plan.ResourceInstances.IsUnknown() != state.ResourceInstances.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() { + if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.MasterAccessList.IsNull() != state.MasterAccessList.IsNull() || plan.MasterAccessList.IsUnknown() != state.MasterAccessList.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.MasterAccessList.IsNull() && !plan.MasterAccessList.IsUnknown() && !state.MasterAccessList.IsNull() && !state.MasterAccessList.IsUnknown() { + if !strings.EqualFold(plan.MasterAccessList.ValueString(), state.MasterAccessList.ValueString()) { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.IpSpaceNameMaster.IsNull() != state.IpSpaceNameMaster.IsNull() || plan.IpSpaceNameMaster.IsUnknown() != state.IpSpaceNameMaster.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.IpSpaceNameMaster.IsNull() && !plan.IpSpaceNameMaster.IsUnknown() && !state.IpSpaceNameMaster.IsNull() && !state.IpSpaceNameMaster.IsUnknown() { + if !strings.EqualFold(plan.IpSpaceNameMaster.ValueString(), state.IpSpaceNameMaster.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 304: resources_core.FormatInt64(plan.ResourceCPU), + 305: resources_core.FormatInt64(plan.ResourceMemory), + 306: resources_core.FormatInt64(plan.ResourceDisk), + 307: resources_core.FormatInt64(plan.ResourceInstances), + 308: resources_core.FormatString(plan.MasterAccessList), + 545: resources_core.FormatString(plan.IpSpaceNameMaster), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 93, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "ipSpaceNameMaster", Field: "IpSpaceNameMaster", Type: "string"}, + {Code: "masterAccessList", Field: "MasterAccessList", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *RabbitmqResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state RabbitmqModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *RabbitmqResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *RabbitmqResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/94_lucee_resource.go b/generated/dev/go/94_lucee_resource.go new file mode 100644 index 0000000..d78129f --- /dev/null +++ b/generated/dev/go/94_lucee_resource.go @@ -0,0 +1,449 @@ +package resources_gen + +import ( + "context" + "fmt" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: lucee +// Service ID: 94 + +var _ resource.Resource = &LuceeResource{} +var _ resource.ResourceWithModifyPlan = &LuceeResource{} +var _ resource.ResourceWithImportState = &LuceeResource{} + +type LuceeResource struct { + client *core.UniversalClient +} + +// LuceeAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration. +type LuceeAccessConfigurationModel struct { + Domain types.String `tfsdk:"domain" json:"domain"` +} + +// LuceeAppConfigurationModel — вложенная модель для map-fixed параметра appConfiguration. +type LuceeAppConfigurationModel struct { + Version types.String `tfsdk:"version" json:"version"` + GitPath types.String `tfsdk:"git_path" json:"gitPath"` + HealthPath types.String `tfsdk:"health_path" json:"healthPath"` +} + +// LuceeClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type LuceeClusterConfigurationModel struct { + Cpu types.Int64 `tfsdk:"cpu" json:"cpu"` + Memory types.Int64 `tfsdk:"memory" json:"memory"` + Replicas types.Int64 `tfsdk:"replicas" json:"replicas"` +} + +// LuceeStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type LuceeStartupConfigurationModel struct { + ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"` +} + +type LuceeModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + // --- Redeploy support (ARCHITECTURE.md) --- + // git_revision: при изменении вызывает redeploy вместо modify. + // Опциональное поле — если не задано, modify работает как обычно. + GitRevision types.String `tfsdk:"git_revision"` + AccessConfiguration *LuceeAccessConfigurationModel `tfsdk:"access_configuration"` + AppConfiguration *LuceeAppConfigurationModel `tfsdk:"app_configuration"` + ClusterConfiguration *LuceeClusterConfigurationModel `tfsdk:"cluster_configuration"` + JsonEnv types.String `tfsdk:"json_env"` + StartupConfiguration *LuceeStartupConfigurationModel `tfsdk:"startup_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewLuceeResource() resource.Resource { + return &LuceeResource{} +} + +func (r *LuceeResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_lucee" +} + +func (r *LuceeResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("lucee-kctea8wd"), MarkdownDescription: "Если указан FQDN в формате: `myapp.example.com`, предварительно будет проверяться DNS-запись (А-запись **должна смотреть на IP Ingress кластера ресурсной платформы**)

Если указан в формате `myapp`, DNS-запись будет автоматически создана в служебной DNS-зоне"}, + }}, + "app_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("6.0")}, + "git_path": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("https://github.com/xahys/testlucee"), MarkdownDescription: "Путь до репозитория приложения
Обязательно должен быть передан как URL: `https://....`

Должен быть сетевой доступ до URL для скачивания исходников"}, + "health_path": schema.StringAttribute{Optional: true, MarkdownDescription: "Путь до healtcheck для запуска пода в Kubernetes, который отдаст 200 код
Пустое значение означает, что будет проверяться исключительно доступность приложения по порту"}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(200), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"}, + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(256), MarkdownDescription: "Указывается в Megabytes"}, + "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)}, + }}, + "json_env": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("{}"), MarkdownDescription: "Передаётся в формате КЛЮЧ:ЗНАЧЕНИЕ"}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"}, + }}, + // --- Redeploy support (ARCHITECTURE.md) --- + // При изменении git_revision вызывается redeploy вместо modify. + // Если не задано — modify работает без изменений. + "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *LuceeResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *LuceeModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *LuceeModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan LuceeModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 1019: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}), + 1020: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64())}), + 1021: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", config.AccessConfiguration.Domain.ValueString())}), + 1022: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", config.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", config.AppConfiguration.HealthPath.ValueString())}), + 1023: resources_core.FormatString(config.JsonEnv), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 94, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *LuceeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data LuceeModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 94, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 1019: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}), + 1020: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64())}), + 1021: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", data.AccessConfiguration.Domain.ValueString())}), + 1022: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", data.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", data.AppConfiguration.HealthPath.ValueString())}), + 1023: resources_core.FormatString(data.JsonEnv), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 94, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 94, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "jsonEnv", Field: "JsonEnv", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *LuceeResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state LuceeModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 94, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "jsonEnv", Field: "JsonEnv", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *LuceeResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan LuceeModel + var state LuceeModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.JsonEnv.IsNull() != state.JsonEnv.IsNull() || plan.JsonEnv.IsUnknown() != state.JsonEnv.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.JsonEnv.IsNull() && !plan.JsonEnv.IsUnknown() && !state.JsonEnv.IsNull() && !state.JsonEnv.IsUnknown() { + if !strings.EqualFold(plan.JsonEnv.ValueString(), state.JsonEnv.ValueString()) { + hasServiceParamChanges = true + } + } + } + // --- Redeploy support (ARCHITECTURE.md) --- + // Проверяем изменился ли git_revision. + // Если да — вызываем redeploy вместо modify. + // git_revision = "" (не задано) → обычный modify. + redeployRequested := false + if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() { + if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() { + redeployRequested = true + } + } + + if !hasServiceParamChanges && !redeployRequested { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + // --- Redeploy support (ARCHITECTURE.md) --- + if redeployRequested { + // Вызываем redeploy — пустой params (операция не требует CFS-параметров) + if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + } else { + params := map[int]string{ + 1024: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64())}), + 1025: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", plan.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", plan.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", plan.AppConfiguration.HealthPath.ValueString())}), + 1026: resources_core.FormatString(plan.JsonEnv), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 94, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "jsonEnv", Field: "JsonEnv", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *LuceeResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state LuceeModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *LuceeResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *LuceeResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/95_nodejs_resource.go b/generated/dev/go/95_nodejs_resource.go new file mode 100644 index 0000000..10900b8 --- /dev/null +++ b/generated/dev/go/95_nodejs_resource.go @@ -0,0 +1,449 @@ +package resources_gen + +import ( + "context" + "fmt" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: nodejs +// Service ID: 95 + +var _ resource.Resource = &NodejsResource{} +var _ resource.ResourceWithModifyPlan = &NodejsResource{} +var _ resource.ResourceWithImportState = &NodejsResource{} + +type NodejsResource struct { + client *core.UniversalClient +} + +// NodejsAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration. +type NodejsAccessConfigurationModel struct { + Domain types.String `tfsdk:"domain" json:"domain"` +} + +// NodejsAppConfigurationModel — вложенная модель для map-fixed параметра appConfiguration. +type NodejsAppConfigurationModel struct { + Version types.String `tfsdk:"version" json:"version"` + GitPath types.String `tfsdk:"git_path" json:"gitPath"` + HealthPath types.String `tfsdk:"health_path" json:"healthPath"` +} + +// NodejsClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type NodejsClusterConfigurationModel struct { + Cpu types.Int64 `tfsdk:"cpu" json:"cpu"` + Memory types.Int64 `tfsdk:"memory" json:"memory"` + Replicas types.Int64 `tfsdk:"replicas" json:"replicas"` +} + +// NodejsStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type NodejsStartupConfigurationModel struct { + ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"` +} + +type NodejsModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + // --- Redeploy support (ARCHITECTURE.md) --- + // git_revision: при изменении вызывает redeploy вместо modify. + // Опциональное поле — если не задано, modify работает как обычно. + GitRevision types.String `tfsdk:"git_revision"` + AccessConfiguration *NodejsAccessConfigurationModel `tfsdk:"access_configuration"` + AppConfiguration *NodejsAppConfigurationModel `tfsdk:"app_configuration"` + ClusterConfiguration *NodejsClusterConfigurationModel `tfsdk:"cluster_configuration"` + JsonEnv types.String `tfsdk:"json_env"` + StartupConfiguration *NodejsStartupConfigurationModel `tfsdk:"startup_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewNodejsResource() resource.Resource { + return &NodejsResource{} +} + +func (r *NodejsResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_nodejs" +} + +func (r *NodejsResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("node-bd1l2h8o"), MarkdownDescription: "Если указан FQDN в формате: `myapp.example.com`, предварительно будет проверяться DNS-запись (А-запись **должна смотреть на IP Ingress кластера ресурсной платформы**)

Если указан в формате `myapp`, DNS-запись будет автоматически создана в служебной DNS-зоне"}, + }}, + "app_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("23")}, + "git_path": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("https://github.com/xahys/testnode.git"), MarkdownDescription: "Путь до репозитория приложения
Обязательно должен быть передан как URL: `https://....`

Должен быть сетевой доступ до URL для скачивания исходников"}, + "health_path": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("1"), MarkdownDescription: "Путь до healtcheck для запуска пода в Kubernetes, который отдаст 200 код
Пустое значение означает, что будет проверяться исключительно доступность приложения по порту"}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(200), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"}, + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(256), MarkdownDescription: "Указывается в Megabytes"}, + "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)}, + }}, + "json_env": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("{}"), MarkdownDescription: "Передаётся в формате КЛЮЧ:ЗНАЧЕНИЕ"}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"}, + }}, + // --- Redeploy support (ARCHITECTURE.md) --- + // При изменении git_revision вызывается redeploy вместо modify. + // Если не задано — modify работает без изменений. + "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *NodejsResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *NodejsModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *NodejsModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan NodejsModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 1028: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}), + 1029: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64())}), + 1030: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", config.AccessConfiguration.Domain.ValueString())}), + 1031: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", config.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", config.AppConfiguration.HealthPath.ValueString())}), + 1032: resources_core.FormatString(config.JsonEnv), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 95, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *NodejsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data NodejsModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 95, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 1028: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}), + 1029: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64())}), + 1030: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", data.AccessConfiguration.Domain.ValueString())}), + 1031: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", data.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", data.AppConfiguration.HealthPath.ValueString())}), + 1032: resources_core.FormatString(data.JsonEnv), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 95, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 95, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "jsonEnv", Field: "JsonEnv", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *NodejsResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state NodejsModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 95, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "jsonEnv", Field: "JsonEnv", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *NodejsResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan NodejsModel + var state NodejsModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.JsonEnv.IsNull() != state.JsonEnv.IsNull() || plan.JsonEnv.IsUnknown() != state.JsonEnv.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.JsonEnv.IsNull() && !plan.JsonEnv.IsUnknown() && !state.JsonEnv.IsNull() && !state.JsonEnv.IsUnknown() { + if !strings.EqualFold(plan.JsonEnv.ValueString(), state.JsonEnv.ValueString()) { + hasServiceParamChanges = true + } + } + } + // --- Redeploy support (ARCHITECTURE.md) --- + // Проверяем изменился ли git_revision. + // Если да — вызываем redeploy вместо modify. + // git_revision = "" (не задано) → обычный modify. + redeployRequested := false + if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() { + if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() { + redeployRequested = true + } + } + + if !hasServiceParamChanges && !redeployRequested { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + // --- Redeploy support (ARCHITECTURE.md) --- + if redeployRequested { + // Вызываем redeploy — пустой params (операция не требует CFS-параметров) + if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + } else { + params := map[int]string{ + 1033: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64())}), + 1034: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", plan.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", plan.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", plan.AppConfiguration.HealthPath.ValueString())}), + 1035: resources_core.FormatString(plan.JsonEnv), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 95, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "jsonEnv", Field: "JsonEnv", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *NodejsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state NodejsModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *NodejsResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *NodejsResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/96_pgadmin_resource.go b/generated/dev/go/96_pgadmin_resource.go new file mode 100644 index 0000000..aac4518 --- /dev/null +++ b/generated/dev/go/96_pgadmin_resource.go @@ -0,0 +1,515 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: pgadmin +// Service ID: 96 + +var _ resource.Resource = &PgadminResource{} +var _ resource.ResourceWithModifyPlan = &PgadminResource{} +var _ resource.ResourceWithImportState = &PgadminResource{} + +type PgadminResource struct { + client *core.UniversalClient +} + +type PgadminModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + // --- Redeploy support (ARCHITECTURE.md) --- + // git_revision: при изменении вызывает redeploy вместо modify. + // Опциональное поле — если не задано, modify работает как обычно. + GitRevision types.String `tfsdk:"git_revision"` + Domain types.String `tfsdk:"domain"` + Login types.String `tfsdk:"login"` + Password types.String `tfsdk:"password"` + ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"` + ResourceDisk types.Int64 `tfsdk:"resource_disk"` + ResourceMemory types.Int64 `tfsdk:"resource_memory"` + ResourceRealm types.String `tfsdk:"resource_realm"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewPgadminResource() resource.Resource { + return &PgadminResource{} +} + +func (r *PgadminResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_pgadmin" +} + +func (r *PgadminResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("pgadmin-1"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."}, + "login": schema.StringAttribute{Required: true, MarkdownDescription: "Логин для подключения к pgAdmin, в формате email (login@email.net) Не изменяется после создания."}, + "password": schema.StringAttribute{Required: true, MarkdownDescription: "Пароль для подключения к pgAdmin Не изменяется после создания.", Sensitive: true}, + "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Квота ядра пода (milicores). 1000Mili = 1 Ядро"}, + "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Квота диска (Указывается в GIGAbytes)"}, + "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Квота памяти пода (MBytes)"}, + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + // --- Redeploy support (ARCHITECTURE.md) --- + // При изменении git_revision вызывается redeploy вместо modify. + // Если не задано — modify работает без изменений. + "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *PgadminResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *PgadminModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *PgadminModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan PgadminModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.Login.IsNull() && !plan.Login.IsUnknown() && !state.Login.IsNull() && !state.Login.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Login.ValueString(), state.Login.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить login", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.Password.IsNull() && !plan.Password.IsUnknown() && !state.Password.IsNull() && !state.Password.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Password.ValueString(), state.Password.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить password", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.Login.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "login is required.") + return + } + if config.Login.IsUnknown() { + return + } + if config.Password.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "password is required.") + return + } + if config.Password.IsUnknown() { + return + } + if config.ResourceRealm.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + return + } + if config.ResourceRealm.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 164: resources_core.FormatString(config.Domain), + 165: resources_core.FormatInt64(config.ResourceCPU), + 166: resources_core.FormatInt64(config.ResourceMemory), + 167: resources_core.FormatInt64(config.ResourceDisk), + 169: resources_core.FormatString(config.ResourceRealm), + 170: resources_core.FormatString(config.Login), + 171: resources_core.FormatString(config.Password), + } + desiredDomain := "" + if !config.Domain.IsNull() && !config.Domain.IsUnknown() { + desiredDomain = config.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 96, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *PgadminResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data PgadminModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.Login.IsNull() || data.Login.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "login is required.") + return + } + if data.Password.IsNull() || data.Password.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "password is required.") + return + } + if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + if !data.Domain.IsNull() && !data.Domain.IsUnknown() { + desiredDomain = data.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 96, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 164: resources_core.FormatString(data.Domain), + 165: resources_core.FormatInt64(data.ResourceCPU), + 166: resources_core.FormatInt64(data.ResourceMemory), + 167: resources_core.FormatInt64(data.ResourceDisk), + 169: resources_core.FormatString(data.ResourceRealm), + 170: resources_core.FormatString(data.Login), + 171: resources_core.FormatString(data.Password), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 96, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 96, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "login", Field: "Login", Type: "string"}, + {Code: "password", Field: "Password", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *PgadminResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state PgadminModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 96, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "login", Field: "Login", Type: "string"}, + {Code: "password", Field: "Password", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *PgadminResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan PgadminModel + var state PgadminModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.ResourceCPU.IsNull() != state.ResourceCPU.IsNull() || plan.ResourceCPU.IsUnknown() != state.ResourceCPU.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() { + if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceMemory.IsNull() != state.ResourceMemory.IsNull() || plan.ResourceMemory.IsUnknown() != state.ResourceMemory.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() { + if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceDisk.IsNull() != state.ResourceDisk.IsNull() || plan.ResourceDisk.IsUnknown() != state.ResourceDisk.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() { + if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() { + hasServiceParamChanges = true + } + } + } + // --- Redeploy support (ARCHITECTURE.md) --- + // Проверяем изменился ли git_revision. + // Если да — вызываем redeploy вместо modify. + // git_revision = "" (не задано) → обычный modify. + redeployRequested := false + if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() { + if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() { + redeployRequested = true + } + } + + if !hasServiceParamChanges && !redeployRequested { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + // --- Redeploy support (ARCHITECTURE.md) --- + if redeployRequested { + // Вызываем redeploy — пустой params (операция не требует CFS-параметров) + if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + } else { + params := map[int]string{ + 172: resources_core.FormatInt64(plan.ResourceCPU), + 173: resources_core.FormatInt64(plan.ResourceMemory), + 174: resources_core.FormatInt64(plan.ResourceDisk), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 96, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "login", Field: "Login", Type: "string"}, + {Code: "password", Field: "Password", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *PgadminResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state PgadminModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *PgadminResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *PgadminResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/97_nodered_resource.go b/generated/dev/go/97_nodered_resource.go new file mode 100644 index 0000000..7b00a75 --- /dev/null +++ b/generated/dev/go/97_nodered_resource.go @@ -0,0 +1,431 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: nodered +// Service ID: 97 + +var _ resource.Resource = &NoderedResource{} +var _ resource.ResourceWithModifyPlan = &NoderedResource{} +var _ resource.ResourceWithImportState = &NoderedResource{} + +type NoderedResource struct { + client *core.UniversalClient +} + +type NoderedModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + Domain types.String `tfsdk:"domain"` + ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"` + ResourceDisk types.Int64 `tfsdk:"resource_disk"` + ResourceInstances types.Int64 `tfsdk:"resource_instances"` + ResourceMemory types.Int64 `tfsdk:"resource_memory"` + ResourceRealm types.String `tfsdk:"resource_realm"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewNoderedResource() resource.Resource { + return &NoderedResource{} +} + +func (r *NoderedResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_nodered" +} + +func (r *NoderedResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("nodered-1"), MarkdownDescription: "Наименование поддомена, будет добавлено в k8stemplate.dev.nubes.ru Не изменяется после создания."}, + "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Квота ядра пода (Указывается в mili). 1000Mili = 1 Ядро Не изменяется после создания."}, + "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Квота диска (Указывается в GIGAbytes) Не изменяется после создания."}, + "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Количество узлов Не изменяется после создания."}, + "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Квота памяти пода (Указывается в Megabytes) Не изменяется после создания."}, + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *NoderedResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *NoderedModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *NoderedModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan NoderedModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() { + if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_c_p_u", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() { + if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_disk", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() { + if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_instances", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() { + if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_memory", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.ResourceRealm.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + return + } + if config.ResourceRealm.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 193: resources_core.FormatInt64(config.ResourceCPU), + 194: resources_core.FormatInt64(config.ResourceMemory), + 195: resources_core.FormatInt64(config.ResourceDisk), + 196: resources_core.FormatString(config.ResourceRealm), + 197: resources_core.FormatString(config.Domain), + 323: resources_core.FormatInt64(config.ResourceInstances), + } + desiredDomain := "" + if !config.Domain.IsNull() && !config.Domain.IsUnknown() { + desiredDomain = config.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 97, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *NoderedResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data NoderedModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + if !data.Domain.IsNull() && !data.Domain.IsUnknown() { + desiredDomain = data.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 97, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 193: resources_core.FormatInt64(data.ResourceCPU), + 194: resources_core.FormatInt64(data.ResourceMemory), + 195: resources_core.FormatInt64(data.ResourceDisk), + 196: resources_core.FormatString(data.ResourceRealm), + 197: resources_core.FormatString(data.Domain), + 323: resources_core.FormatInt64(data.ResourceInstances), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 97, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 97, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *NoderedResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state NoderedModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 97, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *NoderedResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan NoderedModel + var state NoderedModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 97, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *NoderedResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state NoderedModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *NoderedResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *NoderedResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/98_http_resource.go b/generated/dev/go/98_http_resource.go new file mode 100644 index 0000000..5d287fa --- /dev/null +++ b/generated/dev/go/98_http_resource.go @@ -0,0 +1,509 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: http +// Service ID: 98 + +var _ resource.Resource = &HttpResource{} +var _ resource.ResourceWithModifyPlan = &HttpResource{} +var _ resource.ResourceWithImportState = &HttpResource{} + +type HttpResource struct { + client *core.UniversalClient +} + +type HttpModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + // --- Redeploy support (ARCHITECTURE.md) --- + // git_revision: при изменении вызывает redeploy вместо modify. + // Опциональное поле — если не задано, modify работает как обычно. + GitRevision types.String `tfsdk:"git_revision"` + Domain types.String `tfsdk:"domain"` + IngressAnnotations types.String `tfsdk:"ingress_annotations"` + JsonEnv types.String `tfsdk:"json_env"` + RegistryPath types.String `tfsdk:"registry_path"` + ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"` + ResourceInstances types.Int64 `tfsdk:"resource_instances"` + ResourceMemory types.Int64 `tfsdk:"resource_memory"` + ResourceRealm types.String `tfsdk:"resource_realm"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewHttpResource() resource.Resource { + return &HttpResource{} +} + +func (r *HttpResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_http" +} + +func (r *HttpResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("http-1"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."}, + "ingress_annotations": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("{}"), MarkdownDescription: "Формат ключ/значение
Поддерживает пока простые аннотации Не изменяется после создания."}, + "json_env": schema.StringAttribute{Optional: true, MarkdownDescription: "Массив переменных, будут переданы в env приложения Не изменяется после создания."}, + "registry_path": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("nexus-sa.tst.nubes.ru/docker-nubes/jolt"), MarkdownDescription: "Путь до image, который будет установлен как deployment Не изменяется после создания."}, + "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100), MarkdownDescription: "Квота ядра пода (Указывается в mili)"}, + "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Количество узлов"}, + "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(200), MarkdownDescription: "Квота памяти пода (Указывается в Megabytes)"}, + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + // --- Redeploy support (ARCHITECTURE.md) --- + // При изменении git_revision вызывается redeploy вместо modify. + // Если не задано — modify работает без изменений. + "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *HttpResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *HttpModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *HttpModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan HttpModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.IngressAnnotations.IsNull() && !plan.IngressAnnotations.IsUnknown() && !state.IngressAnnotations.IsNull() && !state.IngressAnnotations.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.IngressAnnotations.ValueString(), state.IngressAnnotations.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить ingress_annotations", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.JsonEnv.IsNull() && !plan.JsonEnv.IsUnknown() && !state.JsonEnv.IsNull() && !state.JsonEnv.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.JsonEnv.ValueString(), state.JsonEnv.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить json_env", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.RegistryPath.IsNull() && !plan.RegistryPath.IsUnknown() && !state.RegistryPath.IsNull() && !state.RegistryPath.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.RegistryPath.ValueString(), state.RegistryPath.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить registry_path", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.ResourceRealm.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + return + } + if config.ResourceRealm.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 212: resources_core.FormatString(config.Domain), + 213: resources_core.FormatInt64(config.ResourceCPU), + 214: resources_core.FormatInt64(config.ResourceMemory), + 215: resources_core.FormatInt64(config.ResourceInstances), + 216: resources_core.FormatString(config.ResourceRealm), + 217: resources_core.FormatString(config.RegistryPath), + 241: resources_core.FormatString(config.JsonEnv), + 782: resources_core.FormatString(config.IngressAnnotations), + } + desiredDomain := "" + if !config.Domain.IsNull() && !config.Domain.IsUnknown() { + desiredDomain = config.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 98, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *HttpResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data HttpModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + if !data.Domain.IsNull() && !data.Domain.IsUnknown() { + desiredDomain = data.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 98, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 212: resources_core.FormatString(data.Domain), + 213: resources_core.FormatInt64(data.ResourceCPU), + 214: resources_core.FormatInt64(data.ResourceMemory), + 215: resources_core.FormatInt64(data.ResourceInstances), + 216: resources_core.FormatString(data.ResourceRealm), + 217: resources_core.FormatString(data.RegistryPath), + 241: resources_core.FormatString(data.JsonEnv), + 782: resources_core.FormatString(data.IngressAnnotations), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 98, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 98, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "ingressAnnotations", Field: "IngressAnnotations", Type: "string"}, + {Code: "jsonEnv", Field: "JsonEnv", Type: "string"}, + {Code: "registryPath", Field: "RegistryPath", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *HttpResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state HttpModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 98, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "ingressAnnotations", Field: "IngressAnnotations", Type: "string"}, + {Code: "jsonEnv", Field: "JsonEnv", Type: "string"}, + {Code: "registryPath", Field: "RegistryPath", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *HttpResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan HttpModel + var state HttpModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.ResourceCPU.IsNull() != state.ResourceCPU.IsNull() || plan.ResourceCPU.IsUnknown() != state.ResourceCPU.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() { + if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceMemory.IsNull() != state.ResourceMemory.IsNull() || plan.ResourceMemory.IsUnknown() != state.ResourceMemory.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() { + if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceInstances.IsNull() != state.ResourceInstances.IsNull() || plan.ResourceInstances.IsUnknown() != state.ResourceInstances.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() { + if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() { + hasServiceParamChanges = true + } + } + } + // --- Redeploy support (ARCHITECTURE.md) --- + // Проверяем изменился ли git_revision. + // Если да — вызываем redeploy вместо modify. + // git_revision = "" (не задано) → обычный modify. + redeployRequested := false + if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() { + if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() { + redeployRequested = true + } + } + + if !hasServiceParamChanges && !redeployRequested { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + // --- Redeploy support (ARCHITECTURE.md) --- + if redeployRequested { + // Вызываем redeploy — пустой params (операция не требует CFS-параметров) + if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + } else { + params := map[int]string{ + 218: resources_core.FormatInt64(plan.ResourceCPU), + 219: resources_core.FormatInt64(plan.ResourceMemory), + 220: resources_core.FormatInt64(plan.ResourceInstances), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 98, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "ingressAnnotations", Field: "IngressAnnotations", Type: "string"}, + {Code: "jsonEnv", Field: "JsonEnv", Type: "string"}, + {Code: "registryPath", Field: "RegistryPath", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *HttpResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state HttpModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *HttpResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *HttpResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/99_gitea_resource.go b/generated/dev/go/99_gitea_resource.go new file mode 100644 index 0000000..97139b1 --- /dev/null +++ b/generated/dev/go/99_gitea_resource.go @@ -0,0 +1,579 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: gitea +// Service ID: 99 + +var _ resource.Resource = &GiteaResource{} +var _ resource.ResourceWithModifyPlan = &GiteaResource{} +var _ resource.ResourceWithImportState = &GiteaResource{} + +type GiteaResource struct { + client *core.UniversalClient +} + +type GiteaModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + Domain types.String `tfsdk:"domain"` + IngressExtraAnnotations types.String `tfsdk:"ingress_extra_annotations"` + IniServerParams types.String `tfsdk:"ini_server_params"` + PsqlUid types.String `tfsdk:"psql_uid"` + ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"` + ResourceDisk types.Int64 `tfsdk:"resource_disk"` + ResourceInstances types.Int64 `tfsdk:"resource_instances"` + ResourceMemory types.Int64 `tfsdk:"resource_memory"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewGiteaResource() resource.Resource { + return &GiteaResource{} +} + +func (r *GiteaResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_gitea" +} + +func (r *GiteaResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("gitea-1"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."}, + "ingress_extra_annotations": schema.StringAttribute{Optional: true}, + "ini_server_params": schema.StringAttribute{Optional: true}, + "psql_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UUID Услуги k8sPgsql. Пользователя techGitea, базу данных gitea создаёт автоматически Не изменяется после создания."}, + "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Квота ядра пода (Указывается в mili). 1000Mili = 1 Ядро"}, + "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Квота диска (Указывается в GIGAbytes)"}, + "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Количество узлов"}, + "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Квота памяти пода (Указывается в Megabytes)"}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *GiteaResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *GiteaModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *GiteaModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan GiteaModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan. + // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов. + // Resolve (displayName → UUID или uppercase → lowercase) нужен только для + // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка + // "Provider produced invalid plan: planned value does not match config value". + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.PsqlUid.IsNull() && !plan.PsqlUid.IsUnknown() && !state.PsqlUid.IsNull() && !state.PsqlUid.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.PsqlUid.ValueString(), state.PsqlUid.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить psql_uid", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.PsqlUid.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "psql_uid is required.") + return + } + if config.PsqlUid.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + if !config.PsqlUid.IsNull() && !config.PsqlUid.IsUnknown() { + resolvedPsqlUid, err := r.client.ResolveRefSvcParamValue(ctx, 90, config.PsqlUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve psql_uid", err.Error()) + } else if resolvedPsqlUid != "" && resolvedPsqlUid != config.PsqlUid.ValueString() { + config.PsqlUid = types.StringValue(resolvedPsqlUid) + } + } + params := map[int]string{ + 232: resources_core.FormatInt64(config.ResourceCPU), + 233: resources_core.FormatInt64(config.ResourceMemory), + 234: resources_core.FormatInt64(config.ResourceDisk), + 235: resources_core.FormatInt64(config.ResourceInstances), + 237: resources_core.FormatString(config.Domain), + 238: resources_core.FormatString(config.PsqlUid), + 818: resources_core.FormatString(config.IngressExtraAnnotations), + 821: resources_core.FormatString(config.IniServerParams), + } + desiredDomain := "" + if !config.Domain.IsNull() && !config.Domain.IsUnknown() { + desiredDomain = config.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 99, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *GiteaResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data GiteaModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.PsqlUid.IsNull() || data.PsqlUid.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "psql_uid is required.") + return + } + // ══════════════════════════════════════════════════════════════════════════ + // ПРАВИЛО TERRAFORM (официальная документация): + // «If an attribute value is configured, it is NEVER valid to change that + // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что + // написал пользователь). Менять plan запрещено на уровне фреймворка. + // + // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре. + // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный). + // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит: + // «Provider produced inconsistent result after apply». + // + // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот. + // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как + // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API). + // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный + // регистр в state через strings.EqualFold (сравниваем без учёта регистра). + // ══════════════════════════════════════════════════════════════════════════ + originalPsqlUid := data.PsqlUid + if !data.PsqlUid.IsNull() && !data.PsqlUid.IsUnknown() { + resolvedPsqlUid, err := r.client.ResolveRefSvcParamValue(ctx, 90, data.PsqlUid.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if resolvedPsqlUid != "" && resolvedPsqlUid != data.PsqlUid.ValueString() { + data.PsqlUid = types.StringValue(resolvedPsqlUid) + } + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + if !data.Domain.IsNull() && !data.Domain.IsUnknown() { + desiredDomain = data.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151, 153} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 99, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 232: resources_core.FormatInt64(data.ResourceCPU), + 233: resources_core.FormatInt64(data.ResourceMemory), + 234: resources_core.FormatInt64(data.ResourceDisk), + 235: resources_core.FormatInt64(data.ResourceInstances), + 237: resources_core.FormatString(data.Domain), + 238: resources_core.FormatString(data.PsqlUid), + 818: resources_core.FormatString(data.IngressExtraAnnotations), + 821: resources_core.FormatString(data.IniServerParams), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 99, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 99, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "ingressExtraAnnotations", Field: "IngressExtraAnnotations", Type: "string"}, + {Code: "iniServerParams", Field: "IniServerParams", Type: "string"}, + {Code: "psqlUid", Field: "PsqlUid", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.PsqlUid.IsNull() && !state.PsqlUid.IsUnknown() { + resolvedPsqlUid, err := r.client.ResolveRefSvcParamValue(ctx, 90, state.PsqlUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve psql_uid", err.Error()) + } else if resolvedPsqlUid != "" && resolvedPsqlUid != state.PsqlUid.ValueString() { + state.PsqlUid = types.StringValue(resolvedPsqlUid) + } + } + // Restore user-provided casing в state (Create). + // EqualFold = «совпадают ли значения без учёта регистра?» + // Если да — значит API вернул «туже» строку, только в другом регистре. + // Заменяем state на original (то что было в plan/config пользователя). + // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint. + if !originalPsqlUid.IsNull() && !originalPsqlUid.IsUnknown() && !state.PsqlUid.IsNull() && !state.PsqlUid.IsUnknown() { + if strings.EqualFold(state.PsqlUid.ValueString(), originalPsqlUid.ValueString()) { + state.PsqlUid = originalPsqlUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *GiteaResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state GiteaModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 99, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "ingressExtraAnnotations", Field: "IngressExtraAnnotations", Type: "string"}, + {Code: "iniServerParams", Field: "IniServerParams", Type: "string"}, + {Code: "psqlUid", Field: "PsqlUid", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !newState.PsqlUid.IsNull() && !newState.PsqlUid.IsUnknown() { + resolvedPsqlUid, err := r.client.ResolveRefSvcParamValue(ctx, 90, newState.PsqlUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve psql_uid", err.Error()) + } else if resolvedPsqlUid != "" && resolvedPsqlUid != newState.PsqlUid.ValueString() { + newState.PsqlUid = types.StringValue(resolvedPsqlUid) + } + } + // Restore user-provided casing в state (Read). + // При чтении у нас нет plan — но предыдущий state уже хранит значение + // в регистре пользователя (после первого Create оно было восстановлено). + // Берём prior state (переменная state) как эталон регистра. + // Если API вернул то же UUID только строчными буквами — восстанавливаем. + if !state.PsqlUid.IsNull() && !state.PsqlUid.IsUnknown() && !newState.PsqlUid.IsNull() && !newState.PsqlUid.IsUnknown() { + if strings.EqualFold(newState.PsqlUid.ValueString(), state.PsqlUid.ValueString()) { + newState.PsqlUid = state.PsqlUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *GiteaResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan GiteaModel + var state GiteaModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.ResourceCPU.IsNull() != state.ResourceCPU.IsNull() || plan.ResourceCPU.IsUnknown() != state.ResourceCPU.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() { + if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceMemory.IsNull() != state.ResourceMemory.IsNull() || plan.ResourceMemory.IsUnknown() != state.ResourceMemory.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() { + if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceDisk.IsNull() != state.ResourceDisk.IsNull() || plan.ResourceDisk.IsUnknown() != state.ResourceDisk.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() { + if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceInstances.IsNull() != state.ResourceInstances.IsNull() || plan.ResourceInstances.IsUnknown() != state.ResourceInstances.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() { + if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.IngressExtraAnnotations.IsNull() != state.IngressExtraAnnotations.IsNull() || plan.IngressExtraAnnotations.IsUnknown() != state.IngressExtraAnnotations.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.IngressExtraAnnotations.IsNull() && !plan.IngressExtraAnnotations.IsUnknown() && !state.IngressExtraAnnotations.IsNull() && !state.IngressExtraAnnotations.IsUnknown() { + if !strings.EqualFold(plan.IngressExtraAnnotations.ValueString(), state.IngressExtraAnnotations.ValueString()) { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.IniServerParams.IsNull() != state.IniServerParams.IsNull() || plan.IniServerParams.IsUnknown() != state.IniServerParams.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.IniServerParams.IsNull() && !plan.IniServerParams.IsUnknown() && !state.IniServerParams.IsNull() && !state.IniServerParams.IsUnknown() { + if !strings.EqualFold(plan.IniServerParams.ValueString(), state.IniServerParams.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 259: resources_core.FormatInt64(plan.ResourceCPU), + 260: resources_core.FormatInt64(plan.ResourceMemory), + 261: resources_core.FormatInt64(plan.ResourceDisk), + 262: resources_core.FormatInt64(plan.ResourceInstances), + 819: resources_core.FormatString(plan.IngressExtraAnnotations), + 820: resources_core.FormatString(plan.IniServerParams), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 99, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "ingressExtraAnnotations", Field: "IngressExtraAnnotations", Type: "string"}, + {Code: "iniServerParams", Field: "IniServerParams", Type: "string"}, + {Code: "psqlUid", Field: "PsqlUid", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.PsqlUid.IsNull() && !state.PsqlUid.IsUnknown() { + resolvedPsqlUid, err := r.client.ResolveRefSvcParamValue(ctx, 90, state.PsqlUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve psql_uid", err.Error()) + } else if resolvedPsqlUid != "" && resolvedPsqlUid != state.PsqlUid.ValueString() { + state.PsqlUid = types.StringValue(resolvedPsqlUid) + } + } + // Restore user-provided casing в state (Update). + // После API-вызова modify state содержит значения в lower-case от API. + // Plan == config == то что написал пользователь (регистр неизменён). + // EqualFold: если UUID совпадает без учёта регистра — берём из plan. + if !plan.PsqlUid.IsNull() && !plan.PsqlUid.IsUnknown() && !state.PsqlUid.IsNull() && !state.PsqlUid.IsUnknown() { + if strings.EqualFold(state.PsqlUid.ValueString(), plan.PsqlUid.ValueString()) { + state.PsqlUid = plan.PsqlUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *GiteaResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state GiteaModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *GiteaResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *GiteaResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/dev/go/registry.go b/generated/dev/go/registry.go new file mode 100644 index 0000000..893cf0d --- /dev/null +++ b/generated/dev/go/registry.go @@ -0,0 +1,81 @@ +package resources_gen + +import "github.com/hashicorp/terraform-plugin-framework/resource" + +// Code generated by tools/gen_v2. DO NOT EDIT. +func AllResources() []func() resource.Resource { + return []func() resource.Resource{ + NewAkhqResource, + NewClickhouseResource, + NewDnsrecordResource, + NewDnszoneResource, + NewDummyResource, + NewFlaskResource, + NewGiteaResource, + NewHarborResource, + NewHttpResource, + NewK8sGoResource, + NewK8sOpenbaoResource, + NewK8sSthutrvalClusterResource, + NewK8sVeleroResource, + NewK8sZitiControllerResource, + NewK8svalkeyResource, + NewKafkaResource, + NewLlmAiResource, + NewLuceeResource, + NewMariadbResource, + NewMongodbResource, + NewNextcloudResource, + NewNifiResource, + NewNodejsResource, + NewNoderedResource, + NewOpenwhiskResource, + NewPgadminResource, + NewPostgresResource, + NewRabbitmqResource, + NewRedisResource, + NewS3Resource, + NewS3bucketResource, + NewSupersetResource, + NewTemplateResource, + NewTenantResource, + NewValoTenantResource, + NewVappResource, + NewVcComplexResource, + NewVcMgmtSthutrvalClusterResource, + NewVcNatResource, + NewVcNsxtResource, + NewVcOrgResource, + NewVcOrgSaasResource, + NewVcVdcResource, + NewVcVdcGroupResource, + NewVcVmResource, + NewVcVmV2Resource, + NewVcVmV3Resource, + NewVcexternalipResource, + NewVmpostgreResource, + NewZonesV2Resource, + NewClickhouseDatabaseResource, + NewClickhouseUserResource, + NewK8sOpenbaoKvResource, + NewK8sOpenbaoPolicyResource, + NewK8sOpenbaoSecretsResource, + NewK8sSthutrvalClusterUserResource, + NewK8svalkeyUserResource, + NewKafkaTopicResource, + NewKafkaUserResource, + NewLlmAiSecretsResource, + NewLlmAiTeamResource, + NewMariadbDatabaseResource, + NewMariadbUserResource, + NewMongodbUserResource, + NewNextcloudBackupResource, + NewPostgresBackupResource, + NewPostgresDatabaseResource, + NewPostgresUserResource, + NewS3SubUserResource, + NewVcOrgUserResource, + NewVcVdcGroupVdcResource, + NewZonesV2RecordResource, + } +} diff --git a/generated/prod/go/100_openwhisk_resource.go b/generated/prod/go/100_openwhisk_resource.go new file mode 100644 index 0000000..fa8337a --- /dev/null +++ b/generated/prod/go/100_openwhisk_resource.go @@ -0,0 +1,355 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: openwhisk +// Service ID: 100 + +var _ resource.Resource = &OpenwhiskResource{} +var _ resource.ResourceWithModifyPlan = &OpenwhiskResource{} +var _ resource.ResourceWithImportState = &OpenwhiskResource{} + +type OpenwhiskResource struct { + client *core.UniversalClient +} + +type OpenwhiskModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + Username types.String `tfsdk:"username"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewOpenwhiskResource() resource.Resource { + return &OpenwhiskResource{} +} + +func (r *OpenwhiskResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_openwhisk" +} + +func (r *OpenwhiskResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "username": schema.StringAttribute{Required: true, MarkdownDescription: "Имя пользователя Openwhisk Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *OpenwhiskResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *OpenwhiskModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *OpenwhiskModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan OpenwhiskModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.Username.IsNull() && !plan.Username.IsUnknown() && !state.Username.IsNull() && !state.Username.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Username.ValueString(), state.Username.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить username", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.Username.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "username is required.") + return + } + if config.Username.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 64: resources_core.FormatString(config.Username), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 100, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *OpenwhiskResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data OpenwhiskModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.Username.IsNull() || data.Username.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "username is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 100, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 64: resources_core.FormatString(data.Username), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 100, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 100, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "username", Field: "Username", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *OpenwhiskResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state OpenwhiskModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 100, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "username", Field: "Username", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *OpenwhiskResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan OpenwhiskModel + var state OpenwhiskModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 100, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "username", Field: "Username", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *OpenwhiskResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state OpenwhiskModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *OpenwhiskResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *OpenwhiskResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/109_zones_v2_record_resource.go b/generated/prod/go/109_zones_v2_record_resource.go new file mode 100644 index 0000000..b875e09 --- /dev/null +++ b/generated/prod/go/109_zones_v2_record_resource.go @@ -0,0 +1,366 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: zones_v2 +// Subresource: record + +var _ resource.Resource = &ZonesV2RecordResource{} + +// ZonesV2RecordResource manages a subresource via operations. +type ZonesV2RecordResource struct { + client *core.UniversalClient +} + +type ZonesV2RecordModel struct { + ID types.String `tfsdk:"id"` + ZonesV2ID types.String `tfsdk:"zones_v2_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + RecordInput types.String `tfsdk:"record_input"` + RecordName types.String `tfsdk:"record_name"` + RecordTTL types.Int64 `tfsdk:"record_t_t_l"` + RecordType types.String `tfsdk:"record_type"` +} + +func NewZonesV2RecordResource() resource.Resource { + return &ZonesV2RecordResource{} +} + +func (r *ZonesV2RecordResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_zones_v2_record" +} + +func (r *ZonesV2RecordResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "zones_v2_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "record_input": schema.StringAttribute{Required: true, MarkdownDescription: "Валидация производится только после запуска операции. Если при указанном типе будет указано некорректно значение, это будет описано в логе ошибки операции"}, + "record_name": schema.StringAttribute{Required: true, MarkdownDescription: "Имя зоны в конце проставляется автоматически Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "record_t_t_l": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(3600), MarkdownDescription: "Time-to-Leave записи. Раз в какое время запись будет обновлена"}, + "record_type": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("A")}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *ZonesV2RecordResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan ZonesV2RecordModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if plan.RecordInput.IsNull() || plan.RecordInput.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "record_input is required.") + return + } + if plan.RecordName.IsNull() || plan.RecordName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "record_name is required.") + return + } + + instanceUID := strings.TrimSpace(plan.ZonesV2ID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "recordName": resources_core.FormatString(plan.RecordName), + } + identityCodes := []string{ + "recordName", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("record", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "record", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "recordName": resources_core.FormatString(plan.RecordName), + "recordTTL": resources_core.FormatInt64(plan.RecordTTL), + "recordType": resources_core.FormatString(plan.RecordType), + "recordInput": resources_core.FormatString(plan.RecordInput), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_record", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_record", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "record", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "record", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "record", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *ZonesV2RecordResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state ZonesV2RecordModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *ZonesV2RecordResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan ZonesV2RecordModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "modify_record" == "" { + var state ZonesV2RecordModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.ZonesV2ID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{ + "recordName": resources_core.FormatString(plan.RecordName), + "recordTTL": resources_core.FormatInt64(plan.RecordTTL), + "recordType": resources_core.FormatString(plan.RecordType), + "recordInput": resources_core.FormatString(plan.RecordInput), + }) + + idParams := map[string]string{ + "recordName": resources_core.FormatString(plan.RecordName), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "modify_record", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "record", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *ZonesV2RecordResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state ZonesV2RecordModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_record" == "" { + return + } + + instanceUID := strings.TrimSpace(state.ZonesV2ID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "recordName": resources_core.FormatString(state.RecordName), + } + identityCodes := []string{ + "recordName", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("record", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "recordName": resources_core.FormatString(state.RecordName), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_record", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *ZonesV2RecordResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/109_zones_v2_resource.go b/generated/prod/go/109_zones_v2_resource.go new file mode 100644 index 0000000..e15f4b2 --- /dev/null +++ b/generated/prod/go/109_zones_v2_resource.go @@ -0,0 +1,484 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: zones_v2 +// Service ID: 109 + +var _ resource.Resource = &ZonesV2Resource{} +var _ resource.ResourceWithModifyPlan = &ZonesV2Resource{} +var _ resource.ResourceWithImportState = &ZonesV2Resource{} + +type ZonesV2Resource struct { + client *core.UniversalClient +} + +type ZonesV2Model struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + Records types.String `tfsdk:"records"` + ResourceRealm types.String `tfsdk:"resource_realm"` + ZoneExpire types.Int64 `tfsdk:"zone_expire"` + ZoneKind types.String `tfsdk:"zone_kind"` + ZoneMastersForSlave types.String `tfsdk:"zone_masters_for_slave"` + ZoneName types.String `tfsdk:"zone_name"` + ZoneRefresh types.Int64 `tfsdk:"zone_refresh"` + ZoneRetry types.Int64 `tfsdk:"zone_retry"` + ZoneTTL types.Int64 `tfsdk:"zone_t_t_l"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewZonesV2Resource() resource.Resource { + return &ZonesV2Resource{} +} + +func (r *ZonesV2Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_zones_v2" +} + +func (r *ZonesV2Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "records": schema.StringAttribute{Required: true}, + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("powerdns"), MarkdownDescription: "Не изменяется после создания."}, + "zone_expire": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(3600000), MarkdownDescription: "Время в течение которого вторичный DNS будет пытаться завершить синхронизацию зоны с первичным Не изменяется после создания."}, + "zone_kind": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("NATIVE"), MarkdownDescription: "Тип зоны Не изменяется после создания."}, + "zone_masters_for_slave": schema.StringAttribute{Optional: true, MarkdownDescription: "Использовать если выбран тип SLAVE. IP адреса в формате через пробел для настройки Master-серверов Не изменяется после создания."}, + "zone_name": schema.StringAttribute{Required: true, MarkdownDescription: "Имя зоны Не изменяется после создания."}, + "zone_refresh": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(86400), MarkdownDescription: "Время ожидания ответа вторичного DNS перед запросом SOA-записи с первичных серверов Не изменяется после создания."}, + "zone_retry": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(7200), MarkdownDescription: "Время в течение которого сервер повторно попробует синхронизировать информацию с первичных DNS-серверов Не изменяется после создания."}, + "zone_t_t_l": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(86400), MarkdownDescription: "Время, в течение которого информация будет кешироваться другими DNS-серверами Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *ZonesV2Resource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *ZonesV2Model + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *ZonesV2Model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan ZonesV2Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneExpire.IsNull() && !plan.ZoneExpire.IsUnknown() && !state.ZoneExpire.IsNull() && !state.ZoneExpire.IsUnknown() { + if plan.ZoneExpire.ValueInt64() != state.ZoneExpire.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить zone_expire", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneKind.IsNull() && !plan.ZoneKind.IsUnknown() && !state.ZoneKind.IsNull() && !state.ZoneKind.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ZoneKind.ValueString(), state.ZoneKind.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить zone_kind", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneMastersForSlave.IsNull() && !plan.ZoneMastersForSlave.IsUnknown() && !state.ZoneMastersForSlave.IsNull() && !state.ZoneMastersForSlave.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ZoneMastersForSlave.ValueString(), state.ZoneMastersForSlave.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить zone_masters_for_slave", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneName.IsNull() && !plan.ZoneName.IsUnknown() && !state.ZoneName.IsNull() && !state.ZoneName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ZoneName.ValueString(), state.ZoneName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить zone_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneRefresh.IsNull() && !plan.ZoneRefresh.IsUnknown() && !state.ZoneRefresh.IsNull() && !state.ZoneRefresh.IsUnknown() { + if plan.ZoneRefresh.ValueInt64() != state.ZoneRefresh.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить zone_refresh", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneRetry.IsNull() && !plan.ZoneRetry.IsUnknown() && !state.ZoneRetry.IsNull() && !state.ZoneRetry.IsUnknown() { + if plan.ZoneRetry.ValueInt64() != state.ZoneRetry.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить zone_retry", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneTTL.IsNull() && !plan.ZoneTTL.IsUnknown() && !state.ZoneTTL.IsNull() && !state.ZoneTTL.IsUnknown() { + if plan.ZoneTTL.ValueInt64() != state.ZoneTTL.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить zone_t_t_l", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.Records.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "records is required.") + return + } + if config.Records.IsUnknown() { + return + } + if config.ZoneName.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "zone_name is required.") + return + } + if config.ZoneName.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 761: resources_core.FormatString(config.ZoneName), + 762: resources_core.FormatString(config.ZoneKind), + 763: resources_core.FormatString(config.ZoneMastersForSlave), + 764: resources_core.FormatInt64(config.ZoneRefresh), + 765: resources_core.FormatInt64(config.ZoneRetry), + 766: resources_core.FormatInt64(config.ZoneExpire), + 767: resources_core.FormatInt64(config.ZoneTTL), + 768: resources_core.FormatString(config.ResourceRealm), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 109, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *ZonesV2Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data ZonesV2Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.Records.IsNull() || data.Records.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "records is required.") + return + } + if data.ZoneName.IsNull() || data.ZoneName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "zone_name is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 109, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 761: resources_core.FormatString(data.ZoneName), + 762: resources_core.FormatString(data.ZoneKind), + 763: resources_core.FormatString(data.ZoneMastersForSlave), + 764: resources_core.FormatInt64(data.ZoneRefresh), + 765: resources_core.FormatInt64(data.ZoneRetry), + 766: resources_core.FormatInt64(data.ZoneExpire), + 767: resources_core.FormatInt64(data.ZoneTTL), + 768: resources_core.FormatString(data.ResourceRealm), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 109, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 109, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "records", Field: "Records", Type: "array-map-fixed"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "zoneExpire", Field: "ZoneExpire", Type: "int64"}, + {Code: "zoneKind", Field: "ZoneKind", Type: "string"}, + {Code: "zoneMastersForSlave", Field: "ZoneMastersForSlave", Type: "string"}, + {Code: "zoneName", Field: "ZoneName", Type: "string"}, + {Code: "zoneRefresh", Field: "ZoneRefresh", Type: "int64"}, + {Code: "zoneRetry", Field: "ZoneRetry", Type: "int64"}, + {Code: "zoneTTL", Field: "ZoneTTL", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *ZonesV2Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state ZonesV2Model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 109, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "records", Field: "Records", Type: "array-map-fixed"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "zoneExpire", Field: "ZoneExpire", Type: "int64"}, + {Code: "zoneKind", Field: "ZoneKind", Type: "string"}, + {Code: "zoneMastersForSlave", Field: "ZoneMastersForSlave", Type: "string"}, + {Code: "zoneName", Field: "ZoneName", Type: "string"}, + {Code: "zoneRefresh", Field: "ZoneRefresh", Type: "int64"}, + {Code: "zoneRetry", Field: "ZoneRetry", Type: "int64"}, + {Code: "zoneTTL", Field: "ZoneTTL", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *ZonesV2Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan ZonesV2Model + var state ZonesV2Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.Records.IsNull() != state.Records.IsNull() || plan.Records.IsUnknown() != state.Records.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.Records.IsNull() && !plan.Records.IsUnknown() && !state.Records.IsNull() && !state.Records.IsUnknown() { + if !strings.EqualFold(plan.Records.ValueString(), state.Records.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 769: resources_core.FormatString(plan.Records), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 109, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "records", Field: "Records", Type: "array-map-fixed"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "zoneExpire", Field: "ZoneExpire", Type: "int64"}, + {Code: "zoneKind", Field: "ZoneKind", Type: "string"}, + {Code: "zoneMastersForSlave", Field: "ZoneMastersForSlave", Type: "string"}, + {Code: "zoneName", Field: "ZoneName", Type: "string"}, + {Code: "zoneRefresh", Field: "ZoneRefresh", Type: "int64"}, + {Code: "zoneRetry", Field: "ZoneRetry", Type: "int64"}, + {Code: "zoneTTL", Field: "ZoneTTL", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *ZonesV2Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state ZonesV2Model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *ZonesV2Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *ZonesV2Resource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/110_dnszone_resource.go b/generated/prod/go/110_dnszone_resource.go new file mode 100644 index 0000000..fc3170b --- /dev/null +++ b/generated/prod/go/110_dnszone_resource.go @@ -0,0 +1,495 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: dnszone +// Service ID: 110 + +var _ resource.Resource = &DnszoneResource{} +var _ resource.ResourceWithModifyPlan = &DnszoneResource{} +var _ resource.ResourceWithImportState = &DnszoneResource{} + +type DnszoneResource struct { + client *core.UniversalClient +} + +type DnszoneModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + ResourceRealm types.String `tfsdk:"resource_realm"` + ZoneEmail types.String `tfsdk:"zone_email"` + ZoneKind types.String `tfsdk:"zone_kind"` + ZoneMastersForSlave types.String `tfsdk:"zone_masters_for_slave"` + ZoneName types.String `tfsdk:"zone_name"` + ZoneTimeToExpire types.String `tfsdk:"zone_time_to_expire"` + ZoneTimeToLive types.String `tfsdk:"zone_time_to_live"` + ZoneTimeToRefresh types.String `tfsdk:"zone_time_to_refresh"` + ZoneTimeToRetry types.String `tfsdk:"zone_time_to_retry"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewDnszoneResource() resource.Resource { + return &DnszoneResource{} +} + +func (r *DnszoneResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_dnszone" +} + +func (r *DnszoneResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("powerdns"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + "zone_email": schema.StringAttribute{Required: true, MarkdownDescription: "Контактный e-mail Не изменяется после создания."}, + "zone_kind": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("NATIVE"), MarkdownDescription: "Тип зоны Не изменяется после создания."}, + "zone_masters_for_slave": schema.StringAttribute{Optional: true, MarkdownDescription: "Использовать если выбран тип SLAVE. IP адреса в формате через пробел для настройки Master-серверов Не изменяется после создания."}, + "zone_name": schema.StringAttribute{Required: true, MarkdownDescription: "Имя зоны Не изменяется после создания."}, + "zone_time_to_expire": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("3600000"), MarkdownDescription: "Время в течение которого вторичный DNS будет пытаться завершить синхронизацию зоны с первичным Не изменяется после создания."}, + "zone_time_to_live": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("86400"), MarkdownDescription: "Время, в течение которого информация будет кешироваться другими DNS-серверами Не изменяется после создания."}, + "zone_time_to_refresh": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("86400"), MarkdownDescription: "Время ожидания ответа вторичного DNS перед запросом SOA-записи с первичных серверов Не изменяется после создания."}, + "zone_time_to_retry": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("7200"), MarkdownDescription: "Время в течение которого сервер повторно попробует синхронизировать информацию с первичных DNS-серверов Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *DnszoneResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *DnszoneModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *DnszoneModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan DnszoneModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneEmail.IsNull() && !plan.ZoneEmail.IsUnknown() && !state.ZoneEmail.IsNull() && !state.ZoneEmail.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ZoneEmail.ValueString(), state.ZoneEmail.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить zone_email", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneKind.IsNull() && !plan.ZoneKind.IsUnknown() && !state.ZoneKind.IsNull() && !state.ZoneKind.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ZoneKind.ValueString(), state.ZoneKind.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить zone_kind", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneMastersForSlave.IsNull() && !plan.ZoneMastersForSlave.IsUnknown() && !state.ZoneMastersForSlave.IsNull() && !state.ZoneMastersForSlave.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ZoneMastersForSlave.ValueString(), state.ZoneMastersForSlave.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить zone_masters_for_slave", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneName.IsNull() && !plan.ZoneName.IsUnknown() && !state.ZoneName.IsNull() && !state.ZoneName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ZoneName.ValueString(), state.ZoneName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить zone_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneTimeToExpire.IsNull() && !plan.ZoneTimeToExpire.IsUnknown() && !state.ZoneTimeToExpire.IsNull() && !state.ZoneTimeToExpire.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ZoneTimeToExpire.ValueString(), state.ZoneTimeToExpire.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить zone_time_to_expire", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneTimeToLive.IsNull() && !plan.ZoneTimeToLive.IsUnknown() && !state.ZoneTimeToLive.IsNull() && !state.ZoneTimeToLive.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ZoneTimeToLive.ValueString(), state.ZoneTimeToLive.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить zone_time_to_live", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneTimeToRefresh.IsNull() && !plan.ZoneTimeToRefresh.IsUnknown() && !state.ZoneTimeToRefresh.IsNull() && !state.ZoneTimeToRefresh.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ZoneTimeToRefresh.ValueString(), state.ZoneTimeToRefresh.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить zone_time_to_refresh", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneTimeToRetry.IsNull() && !plan.ZoneTimeToRetry.IsUnknown() && !state.ZoneTimeToRetry.IsNull() && !state.ZoneTimeToRetry.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ZoneTimeToRetry.ValueString(), state.ZoneTimeToRetry.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить zone_time_to_retry", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.ZoneEmail.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "zone_email is required.") + return + } + if config.ZoneEmail.IsUnknown() { + return + } + if config.ZoneName.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "zone_name is required.") + return + } + if config.ZoneName.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 66: resources_core.FormatString(config.ZoneEmail), + 67: resources_core.FormatString(config.ZoneName), + 68: resources_core.FormatString(config.ZoneKind), + 69: resources_core.FormatString(config.ZoneMastersForSlave), + 70: resources_core.FormatString(config.ZoneTimeToRefresh), + 71: resources_core.FormatString(config.ZoneTimeToRetry), + 72: resources_core.FormatString(config.ZoneTimeToExpire), + 73: resources_core.FormatString(config.ZoneTimeToLive), + 284: resources_core.FormatString(config.ResourceRealm), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 110, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *DnszoneResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data DnszoneModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.ZoneEmail.IsNull() || data.ZoneEmail.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "zone_email is required.") + return + } + if data.ZoneName.IsNull() || data.ZoneName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "zone_name is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 110, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 66: resources_core.FormatString(data.ZoneEmail), + 67: resources_core.FormatString(data.ZoneName), + 68: resources_core.FormatString(data.ZoneKind), + 69: resources_core.FormatString(data.ZoneMastersForSlave), + 70: resources_core.FormatString(data.ZoneTimeToRefresh), + 71: resources_core.FormatString(data.ZoneTimeToRetry), + 72: resources_core.FormatString(data.ZoneTimeToExpire), + 73: resources_core.FormatString(data.ZoneTimeToLive), + 284: resources_core.FormatString(data.ResourceRealm), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 110, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 110, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "zoneEmail", Field: "ZoneEmail", Type: "string"}, + {Code: "zoneKind", Field: "ZoneKind", Type: "string"}, + {Code: "zoneMastersForSlave", Field: "ZoneMastersForSlave", Type: "string"}, + {Code: "zoneName", Field: "ZoneName", Type: "string"}, + {Code: "zoneTimeToExpire", Field: "ZoneTimeToExpire", Type: "string"}, + {Code: "zoneTimeToLive", Field: "ZoneTimeToLive", Type: "string"}, + {Code: "zoneTimeToRefresh", Field: "ZoneTimeToRefresh", Type: "string"}, + {Code: "zoneTimeToRetry", Field: "ZoneTimeToRetry", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *DnszoneResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state DnszoneModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 110, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "zoneEmail", Field: "ZoneEmail", Type: "string"}, + {Code: "zoneKind", Field: "ZoneKind", Type: "string"}, + {Code: "zoneMastersForSlave", Field: "ZoneMastersForSlave", Type: "string"}, + {Code: "zoneName", Field: "ZoneName", Type: "string"}, + {Code: "zoneTimeToExpire", Field: "ZoneTimeToExpire", Type: "string"}, + {Code: "zoneTimeToLive", Field: "ZoneTimeToLive", Type: "string"}, + {Code: "zoneTimeToRefresh", Field: "ZoneTimeToRefresh", Type: "string"}, + {Code: "zoneTimeToRetry", Field: "ZoneTimeToRetry", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *DnszoneResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan DnszoneModel + var state DnszoneModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 110, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "zoneEmail", Field: "ZoneEmail", Type: "string"}, + {Code: "zoneKind", Field: "ZoneKind", Type: "string"}, + {Code: "zoneMastersForSlave", Field: "ZoneMastersForSlave", Type: "string"}, + {Code: "zoneName", Field: "ZoneName", Type: "string"}, + {Code: "zoneTimeToExpire", Field: "ZoneTimeToExpire", Type: "string"}, + {Code: "zoneTimeToLive", Field: "ZoneTimeToLive", Type: "string"}, + {Code: "zoneTimeToRefresh", Field: "ZoneTimeToRefresh", Type: "string"}, + {Code: "zoneTimeToRetry", Field: "ZoneTimeToRetry", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *DnszoneResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state DnszoneModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *DnszoneResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *DnszoneResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/111_dnsrecord_resource.go b/generated/prod/go/111_dnsrecord_resource.go new file mode 100644 index 0000000..e5bea0b --- /dev/null +++ b/generated/prod/go/111_dnsrecord_resource.go @@ -0,0 +1,546 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: dnsrecord +// Service ID: 111 + +var _ resource.Resource = &DnsrecordResource{} +var _ resource.ResourceWithModifyPlan = &DnsrecordResource{} +var _ resource.ResourceWithImportState = &DnsrecordResource{} + +type DnsrecordResource struct { + client *core.UniversalClient +} + +type DnsrecordModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + RecordInput types.String `tfsdk:"record_input"` + RecordName types.String `tfsdk:"record_name"` + RecordTTL types.Int64 `tfsdk:"record_t_t_l"` + RecordType types.String `tfsdk:"record_type"` + ZoneUid types.String `tfsdk:"zone_uid"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewDnsrecordResource() resource.Resource { + return &DnsrecordResource{} +} + +func (r *DnsrecordResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_dnsrecord" +} + +func (r *DnsrecordResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "record_input": schema.StringAttribute{Required: true, MarkdownDescription: "Значение. Необходима валидация, в зависимости от типа записи. Должно быть заменено на UUID-внешнийIP"}, + "record_name": schema.StringAttribute{Required: true, MarkdownDescription: "Имя записи. Имя зоны в конце проставляется автоматически Не изменяется после создания."}, + "record_t_t_l": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(3600), MarkdownDescription: "Time-to-Leave записи. Раз в какое время запись будет обновлена Не изменяется после создания."}, + "record_type": schema.StringAttribute{Required: true, MarkdownDescription: "Тип DNS-записи Не изменяется после создания."}, + "zone_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UUID экземпляра услуги DNS-зоны Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *DnsrecordResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *DnsrecordModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *DnsrecordModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan DnsrecordModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan. + // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов. + // Resolve (displayName → UUID или uppercase → lowercase) нужен только для + // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка + // "Provider produced invalid plan: planned value does not match config value". + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.RecordName.IsNull() && !plan.RecordName.IsUnknown() && !state.RecordName.IsNull() && !state.RecordName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.RecordName.ValueString(), state.RecordName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить record_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.RecordTTL.IsNull() && !plan.RecordTTL.IsUnknown() && !state.RecordTTL.IsNull() && !state.RecordTTL.IsUnknown() { + if plan.RecordTTL.ValueInt64() != state.RecordTTL.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить record_t_t_l", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.RecordType.IsNull() && !plan.RecordType.IsUnknown() && !state.RecordType.IsNull() && !state.RecordType.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.RecordType.ValueString(), state.RecordType.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить record_type", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneUid.IsNull() && !plan.ZoneUid.IsUnknown() && !state.ZoneUid.IsNull() && !state.ZoneUid.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ZoneUid.ValueString(), state.ZoneUid.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить zone_uid", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.RecordInput.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "record_input is required.") + return + } + if config.RecordInput.IsUnknown() { + return + } + if config.RecordName.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "record_name is required.") + return + } + if config.RecordName.IsUnknown() { + return + } + if config.RecordType.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "record_type is required.") + return + } + if config.RecordType.IsUnknown() { + return + } + if config.ZoneUid.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "zone_uid is required.") + return + } + if config.ZoneUid.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + if !config.ZoneUid.IsNull() && !config.ZoneUid.IsUnknown() { + resolvedZoneUid, err := r.client.ResolveRefSvcParamValue(ctx, 109, config.ZoneUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve zone_uid", err.Error()) + } else if resolvedZoneUid != "" && resolvedZoneUid != config.ZoneUid.ValueString() { + config.ZoneUid = types.StringValue(resolvedZoneUid) + } + } + params := map[int]string{ + 75: resources_core.FormatString(config.ZoneUid), + 76: resources_core.FormatString(config.RecordType), + 77: resources_core.FormatString(config.RecordName), + 78: resources_core.FormatString(config.RecordInput), + 450: resources_core.FormatInt64(config.RecordTTL), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 111, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *DnsrecordResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data DnsrecordModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.RecordInput.IsNull() || data.RecordInput.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "record_input is required.") + return + } + if data.RecordName.IsNull() || data.RecordName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "record_name is required.") + return + } + if data.RecordType.IsNull() || data.RecordType.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "record_type is required.") + return + } + if data.ZoneUid.IsNull() || data.ZoneUid.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "zone_uid is required.") + return + } + // ══════════════════════════════════════════════════════════════════════════ + // ПРАВИЛО TERRAFORM (официальная документация): + // «If an attribute value is configured, it is NEVER valid to change that + // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что + // написал пользователь). Менять plan запрещено на уровне фреймворка. + // + // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре. + // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный). + // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит: + // «Provider produced inconsistent result after apply». + // + // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот. + // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как + // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API). + // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный + // регистр в state через strings.EqualFold (сравниваем без учёта регистра). + // ══════════════════════════════════════════════════════════════════════════ + originalZoneUid := data.ZoneUid + if !data.ZoneUid.IsNull() && !data.ZoneUid.IsUnknown() { + resolvedZoneUid, err := r.client.ResolveRefSvcParamValue(ctx, 109, data.ZoneUid.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if resolvedZoneUid != "" && resolvedZoneUid != data.ZoneUid.ValueString() { + data.ZoneUid = types.StringValue(resolvedZoneUid) + } + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 111, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 75: resources_core.FormatString(data.ZoneUid), + 76: resources_core.FormatString(data.RecordType), + 77: resources_core.FormatString(data.RecordName), + 78: resources_core.FormatString(data.RecordInput), + 450: resources_core.FormatInt64(data.RecordTTL), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 111, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 111, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "recordInput", Field: "RecordInput", Type: "string"}, + {Code: "recordName", Field: "RecordName", Type: "string"}, + {Code: "recordTTL", Field: "RecordTTL", Type: "int64"}, + {Code: "recordType", Field: "RecordType", Type: "string"}, + {Code: "zoneUid", Field: "ZoneUid", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.ZoneUid.IsNull() && !state.ZoneUid.IsUnknown() { + resolvedZoneUid, err := r.client.ResolveRefSvcParamValue(ctx, 109, state.ZoneUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve zone_uid", err.Error()) + } else if resolvedZoneUid != "" && resolvedZoneUid != state.ZoneUid.ValueString() { + state.ZoneUid = types.StringValue(resolvedZoneUid) + } + } + // Restore user-provided casing в state (Create). + // EqualFold = «совпадают ли значения без учёта регистра?» + // Если да — значит API вернул «туже» строку, только в другом регистре. + // Заменяем state на original (то что было в plan/config пользователя). + // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint. + if !originalZoneUid.IsNull() && !originalZoneUid.IsUnknown() && !state.ZoneUid.IsNull() && !state.ZoneUid.IsUnknown() { + if strings.EqualFold(state.ZoneUid.ValueString(), originalZoneUid.ValueString()) { + state.ZoneUid = originalZoneUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *DnsrecordResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state DnsrecordModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 111, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "recordInput", Field: "RecordInput", Type: "string"}, + {Code: "recordName", Field: "RecordName", Type: "string"}, + {Code: "recordTTL", Field: "RecordTTL", Type: "int64"}, + {Code: "recordType", Field: "RecordType", Type: "string"}, + {Code: "zoneUid", Field: "ZoneUid", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !newState.ZoneUid.IsNull() && !newState.ZoneUid.IsUnknown() { + resolvedZoneUid, err := r.client.ResolveRefSvcParamValue(ctx, 109, newState.ZoneUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve zone_uid", err.Error()) + } else if resolvedZoneUid != "" && resolvedZoneUid != newState.ZoneUid.ValueString() { + newState.ZoneUid = types.StringValue(resolvedZoneUid) + } + } + // Restore user-provided casing в state (Read). + // При чтении у нас нет plan — но предыдущий state уже хранит значение + // в регистре пользователя (после первого Create оно было восстановлено). + // Берём prior state (переменная state) как эталон регистра. + // Если API вернул то же UUID только строчными буквами — восстанавливаем. + if !state.ZoneUid.IsNull() && !state.ZoneUid.IsUnknown() && !newState.ZoneUid.IsNull() && !newState.ZoneUid.IsUnknown() { + if strings.EqualFold(newState.ZoneUid.ValueString(), state.ZoneUid.ValueString()) { + newState.ZoneUid = state.ZoneUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *DnsrecordResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan DnsrecordModel + var state DnsrecordModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.RecordInput.IsNull() != state.RecordInput.IsNull() || plan.RecordInput.IsUnknown() != state.RecordInput.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.RecordInput.IsNull() && !plan.RecordInput.IsUnknown() && !state.RecordInput.IsNull() && !state.RecordInput.IsUnknown() { + if !strings.EqualFold(plan.RecordInput.ValueString(), state.RecordInput.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 256: resources_core.FormatString(plan.RecordInput), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 111, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "recordInput", Field: "RecordInput", Type: "string"}, + {Code: "recordName", Field: "RecordName", Type: "string"}, + {Code: "recordTTL", Field: "RecordTTL", Type: "int64"}, + {Code: "recordType", Field: "RecordType", Type: "string"}, + {Code: "zoneUid", Field: "ZoneUid", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.ZoneUid.IsNull() && !state.ZoneUid.IsUnknown() { + resolvedZoneUid, err := r.client.ResolveRefSvcParamValue(ctx, 109, state.ZoneUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve zone_uid", err.Error()) + } else if resolvedZoneUid != "" && resolvedZoneUid != state.ZoneUid.ValueString() { + state.ZoneUid = types.StringValue(resolvedZoneUid) + } + } + // Restore user-provided casing в state (Update). + // После API-вызова modify state содержит значения в lower-case от API. + // Plan == config == то что написал пользователь (регистр неизменён). + // EqualFold: если UUID совпадает без учёта регистра — берём из plan. + if !plan.ZoneUid.IsNull() && !plan.ZoneUid.IsUnknown() && !state.ZoneUid.IsNull() && !state.ZoneUid.IsUnknown() { + if strings.EqualFold(state.ZoneUid.ValueString(), plan.ZoneUid.ValueString()) { + state.ZoneUid = plan.ZoneUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *DnsrecordResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state DnsrecordModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *DnsrecordResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *DnsrecordResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/112_tenant_resource.go b/generated/prod/go/112_tenant_resource.go new file mode 100644 index 0000000..ec395dd --- /dev/null +++ b/generated/prod/go/112_tenant_resource.go @@ -0,0 +1,372 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: tenant +// Service ID: 112 + +var _ resource.Resource = &TenantResource{} +var _ resource.ResourceWithModifyPlan = &TenantResource{} +var _ resource.ResourceWithImportState = &TenantResource{} + +type TenantResource struct { + client *core.UniversalClient +} + +type TenantModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + GrafanaOrgName types.String `tfsdk:"grafana_org_name"` + ResourceRealm types.String `tfsdk:"resource_realm"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewTenantResource() resource.Resource { + return &TenantResource{} +} + +func (r *TenantResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_tenant" +} + +func (r *TenantResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "grafana_org_name": schema.StringAttribute{Required: true, MarkdownDescription: "WZ-Код клиента Не изменяется после создания."}, + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("grafana.ngcloud.ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *TenantResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *TenantModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *TenantModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan TenantModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.GrafanaOrgName.IsNull() && !plan.GrafanaOrgName.IsUnknown() && !state.GrafanaOrgName.IsNull() && !state.GrafanaOrgName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.GrafanaOrgName.ValueString(), state.GrafanaOrgName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить grafana_org_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.GrafanaOrgName.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "grafana_org_name is required.") + return + } + if config.GrafanaOrgName.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 239: resources_core.FormatString(config.GrafanaOrgName), + 240: resources_core.FormatString(config.ResourceRealm), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 112, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *TenantResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data TenantModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.GrafanaOrgName.IsNull() || data.GrafanaOrgName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "grafana_org_name is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 112, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 239: resources_core.FormatString(data.GrafanaOrgName), + 240: resources_core.FormatString(data.ResourceRealm), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 112, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 112, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "grafanaOrgName", Field: "GrafanaOrgName", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *TenantResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state TenantModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 112, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "grafanaOrgName", Field: "GrafanaOrgName", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *TenantResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan TenantModel + var state TenantModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 112, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "grafanaOrgName", Field: "GrafanaOrgName", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *TenantResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state TenantModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *TenantResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *TenantResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/113_vc_complex_resource.go b/generated/prod/go/113_vc_complex_resource.go new file mode 100644 index 0000000..69d834d --- /dev/null +++ b/generated/prod/go/113_vc_complex_resource.go @@ -0,0 +1,563 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vc_complex +// Service ID: 113 + +var _ resource.Resource = &VcComplexResource{} +var _ resource.ResourceWithModifyPlan = &VcComplexResource{} +var _ resource.ResourceWithImportState = &VcComplexResource{} + +type VcComplexResource struct { + client *core.UniversalClient +} + +type VcComplexModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + CpuAllocated types.String `tfsdk:"cpu_allocated"` + CpuGuaranteed types.Int64 `tfsdk:"cpu_guaranteed"` + IpSpaceName types.String `tfsdk:"ip_space_name"` + MemAllocated types.Int64 `tfsdk:"mem_allocated"` + MemGuaranteed types.Int64 `tfsdk:"mem_guaranteed"` + NeedEnableAVI types.Bool `tfsdk:"need_enable_a_v_i"` + NeedExternalAddressSNAT types.Bool `tfsdk:"need_external_address_s_n_a_t"` + ResourceRealm types.String `tfsdk:"resource_realm"` + SegroupName types.String `tfsdk:"segroup_name"` + StorageConfig types.String `tfsdk:"storage_config"` + VIPCount types.Int64 `tfsdk:"v_i_p_count"` + VappName types.String `tfsdk:"vapp_name"` + VdcNetworkPool types.String `tfsdk:"vdc_network_pool"` + VdcProviderGateway types.String `tfsdk:"vdc_provider_gateway"` + VirtualServicesCount types.String `tfsdk:"virtual_services_count"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewVcComplexResource() resource.Resource { + return &VcComplexResource{} +} + +func (r *VcComplexResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vc_complex" +} + +func (r *VcComplexResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "cpu_allocated": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("1"), MarkdownDescription: "шт Не изменяется после создания."}, + "cpu_guaranteed": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(20), MarkdownDescription: "Не изменяется после создания."}, + "ip_space_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("internet-ipv4-v1"), MarkdownDescription: "Имя ip Space для внешнего IP. Целесообразнее брать самый дешёвый, так как это vip для исходящего трафика. Необходимо задавать если включен needExternalAddressSNAT Не изменяется после создания."}, + "mem_allocated": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "gb Не изменяется после создания."}, + "mem_guaranteed": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(20), MarkdownDescription: "Не изменяется после создания."}, + "need_enable_a_v_i": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включает AVI в Cloud Director Не изменяется после создания."}, + "need_external_address_s_n_a_t": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Выделяет внешний IP на Edge. Используется для SNAT всего трафика Не изменяется после создания."}, + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("ngcloud.ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + "segroup_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("SEGROUP-SANDBOX-CL1-SHARED-01"), MarkdownDescription: "Наименование segroup, в котором будут выделяться пулы на AVI Не изменяется после создания."}, + "storage_config": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{\"name\": \"\", \"size\": 1024}]"), MarkdownDescription: "Описывается как массив объектов, где описывается имя политики и её размер Не изменяется после создания.", PlanModifiers: []planmodifier.String{resources_core.JsonNormalize()}}, + "v_i_p_count": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(0), MarkdownDescription: "ш Не изменяется после создания."}, + "vapp_name": schema.StringAttribute{Required: true, MarkdownDescription: "Имя vApp Не изменяется после создания."}, + "vdc_network_pool": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("nsxt-sandbox-geneve-np"), MarkdownDescription: "Не изменяется после создания."}, + "vdc_provider_gateway": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("sandbox-v1cl1-pvdc"), MarkdownDescription: "Взять из свойств ресурсной платформы.
Путь .vcd.providerVdc - взять KEY

providerGateway Cloud Director. Влияет ресурсы будут предоставлены. Является полем (List<String>) в свойствах рес. платформы ЛК, но сам параметр передаётся только как один String Не изменяется после создания."}, + "virtual_services_count": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("1"), MarkdownDescription: "Кол-во виртуальных сервисов, которые выделяются на AVI. Во избежании коллапса пока выделяется до 4 Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VcComplexResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *VcComplexModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *VcComplexModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan VcComplexModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.CpuAllocated.IsNull() && !plan.CpuAllocated.IsUnknown() && !state.CpuAllocated.IsNull() && !state.CpuAllocated.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.CpuAllocated.ValueString(), state.CpuAllocated.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить cpu_allocated", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.CpuGuaranteed.IsNull() && !plan.CpuGuaranteed.IsUnknown() && !state.CpuGuaranteed.IsNull() && !state.CpuGuaranteed.IsUnknown() { + if plan.CpuGuaranteed.ValueInt64() != state.CpuGuaranteed.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить cpu_guaranteed", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.IpSpaceName.IsNull() && !plan.IpSpaceName.IsUnknown() && !state.IpSpaceName.IsNull() && !state.IpSpaceName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.IpSpaceName.ValueString(), state.IpSpaceName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить ip_space_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.MemAllocated.IsNull() && !plan.MemAllocated.IsUnknown() && !state.MemAllocated.IsNull() && !state.MemAllocated.IsUnknown() { + if plan.MemAllocated.ValueInt64() != state.MemAllocated.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить mem_allocated", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.MemGuaranteed.IsNull() && !plan.MemGuaranteed.IsUnknown() && !state.MemGuaranteed.IsNull() && !state.MemGuaranteed.IsUnknown() { + if plan.MemGuaranteed.ValueInt64() != state.MemGuaranteed.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить mem_guaranteed", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.NeedEnableAVI.IsNull() && !plan.NeedEnableAVI.IsUnknown() && !state.NeedEnableAVI.IsNull() && !state.NeedEnableAVI.IsUnknown() { + if plan.NeedEnableAVI.ValueBool() != state.NeedEnableAVI.ValueBool() { + resp.Diagnostics.AddError("Нельзя изменить need_enable_a_v_i", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.NeedExternalAddressSNAT.IsNull() && !plan.NeedExternalAddressSNAT.IsUnknown() && !state.NeedExternalAddressSNAT.IsNull() && !state.NeedExternalAddressSNAT.IsUnknown() { + if plan.NeedExternalAddressSNAT.ValueBool() != state.NeedExternalAddressSNAT.ValueBool() { + resp.Diagnostics.AddError("Нельзя изменить need_external_address_s_n_a_t", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.SegroupName.IsNull() && !plan.SegroupName.IsUnknown() && !state.SegroupName.IsNull() && !state.SegroupName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.SegroupName.ValueString(), state.SegroupName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить segroup_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.StorageConfig.IsNull() && !plan.StorageConfig.IsUnknown() && !state.StorageConfig.IsNull() && !state.StorageConfig.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.StorageConfig.ValueString(), state.StorageConfig.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить storage_config", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.VIPCount.IsNull() && !plan.VIPCount.IsUnknown() && !state.VIPCount.IsNull() && !state.VIPCount.IsUnknown() { + if plan.VIPCount.ValueInt64() != state.VIPCount.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить v_i_p_count", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.VappName.IsNull() && !plan.VappName.IsUnknown() && !state.VappName.IsNull() && !state.VappName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VappName.ValueString(), state.VappName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить vapp_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.VdcNetworkPool.IsNull() && !plan.VdcNetworkPool.IsUnknown() && !state.VdcNetworkPool.IsNull() && !state.VdcNetworkPool.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VdcNetworkPool.ValueString(), state.VdcNetworkPool.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить vdc_network_pool", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.VdcProviderGateway.IsNull() && !plan.VdcProviderGateway.IsUnknown() && !state.VdcProviderGateway.IsNull() && !state.VdcProviderGateway.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VdcProviderGateway.ValueString(), state.VdcProviderGateway.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить vdc_provider_gateway", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.VirtualServicesCount.IsNull() && !plan.VirtualServicesCount.IsUnknown() && !state.VirtualServicesCount.IsNull() && !state.VirtualServicesCount.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VirtualServicesCount.ValueString(), state.VirtualServicesCount.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить virtual_services_count", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.VappName.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "vapp_name is required.") + return + } + if config.VappName.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 332: resources_core.FormatString(config.ResourceRealm), + 334: resources_core.FormatString(config.VappName), + 358: resources_core.FormatString(config.VdcProviderGateway), + 359: resources_core.FormatString(config.VirtualServicesCount), + 360: resources_core.FormatString(config.IpSpaceName), + 399: resources_core.FormatString(config.StorageConfig), + 400: resources_core.FormatString(config.VdcNetworkPool), + 401: resources_core.FormatInt64(config.CpuGuaranteed), + 402: resources_core.FormatInt64(config.MemGuaranteed), + 403: resources_core.FormatString(config.SegroupName), + 404: resources_core.FormatBool(config.NeedExternalAddressSNAT), + 405: resources_core.FormatBool(config.NeedEnableAVI), + 566: resources_core.FormatString(config.CpuAllocated), + 567: resources_core.FormatInt64(config.MemAllocated), + 568: resources_core.FormatInt64(config.VIPCount), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 113, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *VcComplexResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data VcComplexModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.VappName.IsNull() || data.VappName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "vapp_name is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 113, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 332: resources_core.FormatString(data.ResourceRealm), + 334: resources_core.FormatString(data.VappName), + 358: resources_core.FormatString(data.VdcProviderGateway), + 359: resources_core.FormatString(data.VirtualServicesCount), + 360: resources_core.FormatString(data.IpSpaceName), + 399: resources_core.FormatString(data.StorageConfig), + 400: resources_core.FormatString(data.VdcNetworkPool), + 401: resources_core.FormatInt64(data.CpuGuaranteed), + 402: resources_core.FormatInt64(data.MemGuaranteed), + 403: resources_core.FormatString(data.SegroupName), + 404: resources_core.FormatBool(data.NeedExternalAddressSNAT), + 405: resources_core.FormatBool(data.NeedEnableAVI), + 566: resources_core.FormatString(data.CpuAllocated), + 567: resources_core.FormatInt64(data.MemAllocated), + 568: resources_core.FormatInt64(data.VIPCount), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 113, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 113, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "cpuAllocated", Field: "CpuAllocated", Type: "string"}, + {Code: "cpuGuaranteed", Field: "CpuGuaranteed", Type: "int64"}, + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "memAllocated", Field: "MemAllocated", Type: "int64"}, + {Code: "memGuaranteed", Field: "MemGuaranteed", Type: "int64"}, + {Code: "needEnableAVI", Field: "NeedEnableAVI", Type: "bool"}, + {Code: "needExternalAddressSNAT", Field: "NeedExternalAddressSNAT", Type: "bool"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "segroupName", Field: "SegroupName", Type: "string"}, + {Code: "storageConfig", Field: "StorageConfig", Type: "string"}, + {Code: "vIPCount", Field: "VIPCount", Type: "int64"}, + {Code: "vappName", Field: "VappName", Type: "string"}, + {Code: "vdcNetworkPool", Field: "VdcNetworkPool", Type: "string"}, + {Code: "vdcProviderGateway", Field: "VdcProviderGateway", Type: "string"}, + {Code: "virtualServicesCount", Field: "VirtualServicesCount", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcComplexResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VcComplexModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 113, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "cpuAllocated", Field: "CpuAllocated", Type: "string"}, + {Code: "cpuGuaranteed", Field: "CpuGuaranteed", Type: "int64"}, + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "memAllocated", Field: "MemAllocated", Type: "int64"}, + {Code: "memGuaranteed", Field: "MemGuaranteed", Type: "int64"}, + {Code: "needEnableAVI", Field: "NeedEnableAVI", Type: "bool"}, + {Code: "needExternalAddressSNAT", Field: "NeedExternalAddressSNAT", Type: "bool"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "segroupName", Field: "SegroupName", Type: "string"}, + {Code: "storageConfig", Field: "StorageConfig", Type: "string"}, + {Code: "vIPCount", Field: "VIPCount", Type: "int64"}, + {Code: "vappName", Field: "VappName", Type: "string"}, + {Code: "vdcNetworkPool", Field: "VdcNetworkPool", Type: "string"}, + {Code: "vdcProviderGateway", Field: "VdcProviderGateway", Type: "string"}, + {Code: "virtualServicesCount", Field: "VirtualServicesCount", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *VcComplexResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VcComplexModel + var state VcComplexModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 113, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "cpuAllocated", Field: "CpuAllocated", Type: "string"}, + {Code: "cpuGuaranteed", Field: "CpuGuaranteed", Type: "int64"}, + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "memAllocated", Field: "MemAllocated", Type: "int64"}, + {Code: "memGuaranteed", Field: "MemGuaranteed", Type: "int64"}, + {Code: "needEnableAVI", Field: "NeedEnableAVI", Type: "bool"}, + {Code: "needExternalAddressSNAT", Field: "NeedExternalAddressSNAT", Type: "bool"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "segroupName", Field: "SegroupName", Type: "string"}, + {Code: "storageConfig", Field: "StorageConfig", Type: "string"}, + {Code: "vIPCount", Field: "VIPCount", Type: "int64"}, + {Code: "vappName", Field: "VappName", Type: "string"}, + {Code: "vdcNetworkPool", Field: "VdcNetworkPool", Type: "string"}, + {Code: "vdcProviderGateway", Field: "VdcProviderGateway", Type: "string"}, + {Code: "virtualServicesCount", Field: "VirtualServicesCount", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcComplexResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VcComplexModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *VcComplexResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *VcComplexResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/114_gitea_complex_resource.go b/generated/prod/go/114_gitea_complex_resource.go new file mode 100644 index 0000000..f6c0b7c --- /dev/null +++ b/generated/prod/go/114_gitea_complex_resource.go @@ -0,0 +1,322 @@ +package resources_gen + +import ( + "context" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: gitea_complex +// Service ID: 114 + +var _ resource.Resource = &GiteaComplexResource{} +var _ resource.ResourceWithModifyPlan = &GiteaComplexResource{} +var _ resource.ResourceWithImportState = &GiteaComplexResource{} + +type GiteaComplexResource struct { + client *core.UniversalClient +} + +type GiteaComplexModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewGiteaComplexResource() resource.Resource { + return &GiteaComplexResource{} +} + +func (r *GiteaComplexResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_gitea_complex" +} + +func (r *GiteaComplexResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *GiteaComplexResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *GiteaComplexModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *GiteaComplexModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan GiteaComplexModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{} + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 114, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *GiteaComplexResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data GiteaComplexModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 114, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{} + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 114, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 114, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{}) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *GiteaComplexResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state GiteaComplexModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 114, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{}) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *GiteaComplexResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan GiteaComplexModel + var state GiteaComplexModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 114, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{}) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *GiteaComplexResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state GiteaComplexModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *GiteaComplexResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *GiteaComplexResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/115_mariadb_database_resource.go b/generated/prod/go/115_mariadb_database_resource.go new file mode 100644 index 0000000..8ca1ed8 --- /dev/null +++ b/generated/prod/go/115_mariadb_database_resource.go @@ -0,0 +1,343 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: mariadb +// Subresource: database + +var _ resource.Resource = &MariadbDatabaseResource{} + +// MariadbDatabaseResource manages a subresource via operations. +type MariadbDatabaseResource struct { + client *core.UniversalClient +} + +type MariadbDatabaseModel struct { + ID types.String `tfsdk:"id"` + MariadbID types.String `tfsdk:"mariadb_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + DbName types.String `tfsdk:"db_name"` +} + +func NewMariadbDatabaseResource() resource.Resource { + return &MariadbDatabaseResource{} +} + +func (r *MariadbDatabaseResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_mariadb_database" +} + +func (r *MariadbDatabaseResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "mariadb_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "db_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("db-zs0q9wuv"), MarkdownDescription: "Пример: `db01` Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *MariadbDatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan MariadbDatabaseModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.MariadbID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "dbName": resources_core.FormatString(plan.DbName), + } + identityCodes := []string{ + "dbName", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("database", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "dbName": resources_core.FormatString(plan.DbName), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_database", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_database", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *MariadbDatabaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state MariadbDatabaseModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *MariadbDatabaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan MariadbDatabaseModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state MariadbDatabaseModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.MariadbID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{ + "dbName": resources_core.FormatString(plan.DbName), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *MariadbDatabaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state MariadbDatabaseModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_database" == "" { + return + } + + instanceUID := strings.TrimSpace(state.MariadbID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "dbName": resources_core.FormatString(state.DbName), + } + identityCodes := []string{ + "dbName", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("database", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "dbName": resources_core.FormatString(state.DbName), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_database", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *MariadbDatabaseResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/115_mariadb_resource.go b/generated/prod/go/115_mariadb_resource.go new file mode 100644 index 0000000..4e4044f --- /dev/null +++ b/generated/prod/go/115_mariadb_resource.go @@ -0,0 +1,447 @@ +package resources_gen + +import ( + "context" + "fmt" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: mariadb +// Service ID: 115 + +var _ resource.Resource = &MariadbResource{} +var _ resource.ResourceWithModifyPlan = &MariadbResource{} +var _ resource.ResourceWithImportState = &MariadbResource{} + +type MariadbResource struct { + client *core.UniversalClient +} + +// MariadbAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration. +type MariadbAccessConfigurationModel struct { + MasterIpSpace types.String `tfsdk:"master_ip_space" json:"masterIpSpace"` + MasterAccessList types.String `tfsdk:"master_access_list" json:"masterAccessList"` +} + +// MariadbAutoscaleConfigurationModel — вложенная модель для map-fixed параметра autoscaleConfiguration. +type MariadbAutoscaleConfigurationModel struct { + Enabled types.Bool `tfsdk:"enabled" json:"enabled"` + Schedule types.Int64 `tfsdk:"schedule" json:"schedule"` + Quota types.Int64 `tfsdk:"quota" json:"quota"` + Percent types.Int64 `tfsdk:"percent" json:"percent"` +} + +// MariadbBackupConfigurationModel — вложенная модель для map-fixed параметра backupConfiguration. +type MariadbBackupConfigurationModel struct { + S3Uid types.String `tfsdk:"s3_uid" json:"s3Uid"` + Retain types.Int64 `tfsdk:"retain" json:"retain"` + Schedule types.String `tfsdk:"schedule" json:"schedule"` +} + +// MariadbClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type MariadbClusterConfigurationModel struct { + Replicas types.Int64 `tfsdk:"replicas" json:"replicas"` + Disk types.Int64 `tfsdk:"disk" json:"disk"` + Cpu types.Int64 `tfsdk:"cpu" json:"cpu"` + Memory types.Int64 `tfsdk:"memory" json:"memory"` +} + +// MariadbMariadbConfigurationModel — вложенная модель для map-fixed параметра mariadbConfiguration. +type MariadbMariadbConfigurationModel struct { + Version types.String `tfsdk:"version" json:"version"` +} + +// MariadbStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type MariadbStartupConfigurationModel struct { + ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"` +} + +type MariadbModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessConfiguration *MariadbAccessConfigurationModel `tfsdk:"access_configuration"` + AutoscaleConfiguration *MariadbAutoscaleConfigurationModel `tfsdk:"autoscale_configuration"` + BackupConfiguration *MariadbBackupConfigurationModel `tfsdk:"backup_configuration"` + ClusterConfiguration *MariadbClusterConfigurationModel `tfsdk:"cluster_configuration"` + MariadbConfiguration *MariadbMariadbConfigurationModel `tfsdk:"mariadb_configuration"` + StartupConfiguration *MariadbStartupConfigurationModel `tfsdk:"startup_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewMariadbResource() resource.Resource { + return &MariadbResource{} +} + +func (r *MariadbResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_mariadb" +} + +func (r *MariadbResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "master_ip_space": schema.StringAttribute{Required: true, MarkdownDescription: "Из какого Ip-Space резервировать внешний IP
Список предоставляется из Организации, в котором развёрнута ресурсная платформа"}, + "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"}, + }}, + "autoscale_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "enabled": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включает автоскейлинг PV. При включении параметра необходимо настроить autoScalePercentage и autoScaleTechWindow. Алерт при срабатывании вызывает modify у текущей услуги"}, + "schedule": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(0), MarkdownDescription: "Требует включения autoScale. Окно обновления (пока не реализовано). Указывается час в виде Integer (0 / 5 / 12 / 23)"}, + "quota": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100), MarkdownDescription: "До какого размера возможно увеличивать диск, когда включен Autoscale"}, + "percent": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Требует включения autoScale. Позволяет указать в процентах расширение от текущего макс объема
Минимальный размер на который расширяется - **1Gb**"}, + }}, + "backup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "s3_uid": schema.StringAttribute{Required: true, MarkdownDescription: "Экземпляр S3 обязателен"}, + "retain": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(720)}, + "schedule": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("0 0 * * *")}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Пока `Galera` выключена
Как будет запрос на включение и тестирование, реплики можно будет увеличивать"}, + "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Указывается в GIGAbytes"}, + "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"}, + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Указывается в Megabytes"}, + }}, + "mariadb_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("9.4.0")}, + }}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8s-4-ext-nubes-ru"), MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"}, + }}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *MariadbResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *MariadbModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *MariadbModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan MariadbModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 877: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}), + 878: resources_core.BuildJSON(map[string]string{"replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64())}), + 879: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterAccessList.ValueString())}), + 880: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.MariadbConfiguration.Version.ValueString())}), + 881: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", config.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", config.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", config.BackupConfiguration.Schedule.ValueString())}), + 882: resources_core.BuildJSON(map[string]string{"enabled": fmt.Sprintf("%v", config.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", config.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", config.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", config.AutoscaleConfiguration.Percent.ValueInt64())}), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 115, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *MariadbResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data MariadbModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 115, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 877: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}), + 878: resources_core.BuildJSON(map[string]string{"replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64())}), + 879: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterAccessList.ValueString())}), + 880: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.MariadbConfiguration.Version.ValueString())}), + 881: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", data.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", data.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", data.BackupConfiguration.Schedule.ValueString())}), + 882: resources_core.BuildJSON(map[string]string{"enabled": fmt.Sprintf("%v", data.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", data.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", data.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", data.AutoscaleConfiguration.Percent.ValueInt64())}), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 115, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 115, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"}, + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "mariadbConfiguration", Field: "MariadbConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *MariadbResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state MariadbModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 115, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"}, + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "mariadbConfiguration", Field: "MariadbConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *MariadbResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan MariadbModel + var state MariadbModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 883: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64())}), + 884: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterAccessList.ValueString())}), + 885: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", plan.MariadbConfiguration.Version.ValueString())}), + 886: resources_core.BuildJSON(map[string]string{"retain": fmt.Sprintf("%d", plan.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", plan.BackupConfiguration.Schedule.ValueString()), "s3Uid": fmt.Sprintf("\"%s\"", plan.BackupConfiguration.S3Uid.ValueString())}), + 887: resources_core.BuildJSON(map[string]string{"enabled": fmt.Sprintf("%v", plan.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Percent.ValueInt64())}), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 115, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"}, + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "mariadbConfiguration", Field: "MariadbConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *MariadbResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state MariadbModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *MariadbResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *MariadbResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/115_mariadb_user_resource.go b/generated/prod/go/115_mariadb_user_resource.go new file mode 100644 index 0000000..d1e6619 --- /dev/null +++ b/generated/prod/go/115_mariadb_user_resource.go @@ -0,0 +1,367 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: mariadb +// Subresource: user + +var _ resource.Resource = &MariadbUserResource{} + +// MariadbUserResource manages a subresource via operations. +type MariadbUserResource struct { + client *core.UniversalClient +} + +type MariadbUserModel struct { + ID types.String `tfsdk:"id"` + MariadbID types.String `tfsdk:"mariadb_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessHosts types.String `tfsdk:"access_hosts"` + DbName types.String `tfsdk:"db_name"` + Password types.String `tfsdk:"password"` + Username types.String `tfsdk:"username"` +} + +func NewMariadbUserResource() resource.Resource { + return &MariadbUserResource{} +} + +func (r *MariadbUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_mariadb_user" +} + +func (r *MariadbUserResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "mariadb_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_hosts": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("%"), MarkdownDescription: "Пример: `%` Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "db_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Необходимо указать массив существующих баз Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "password": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("abpwcb75"), MarkdownDescription: "Секрет будет записан в хранилище секретов и не будет отображаться публично Не изменяется после создания. Изменение требует пересоздания.", Sensitive: true, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "username": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("usertigstlyt"), MarkdownDescription: "Пример: `myusername` Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *MariadbUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan MariadbUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.MariadbID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "accessHosts": resources_core.FormatString(plan.AccessHosts), + "dbName": resources_core.FormatString(plan.DbName), + "password": resources_core.FormatString(plan.Password), + "username": resources_core.FormatString(plan.Username), + } + identityCodes := []string{ + "accessHosts", + "dbName", + "password", + "username", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "username": resources_core.FormatString(plan.Username), + "password": resources_core.FormatString(plan.Password), + "dbName": resources_core.FormatString(plan.DbName), + "accessHosts": resources_core.FormatString(plan.AccessHosts), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *MariadbUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state MariadbUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *MariadbUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan MariadbUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state MariadbUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.MariadbID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{ + "accessHosts": resources_core.FormatString(plan.AccessHosts), + "dbName": resources_core.FormatString(plan.DbName), + "password": resources_core.FormatString(plan.Password), + "username": resources_core.FormatString(plan.Username), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *MariadbUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state MariadbUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_user" == "" { + return + } + + instanceUID := strings.TrimSpace(state.MariadbID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "accessHosts": resources_core.FormatString(state.AccessHosts), + "dbName": resources_core.FormatString(state.DbName), + "password": resources_core.FormatString(state.Password), + "username": resources_core.FormatString(state.Username), + } + identityCodes := []string{ + "accessHosts", + "dbName", + "password", + "username", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "username": resources_core.FormatString(state.Username), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_user", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *MariadbUserResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/116_kafka_resource.go b/generated/prod/go/116_kafka_resource.go new file mode 100644 index 0000000..7ed0fa5 --- /dev/null +++ b/generated/prod/go/116_kafka_resource.go @@ -0,0 +1,389 @@ +package resources_gen + +import ( + "context" + "fmt" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: kafka +// Service ID: 116 + +var _ resource.Resource = &KafkaResource{} +var _ resource.ResourceWithModifyPlan = &KafkaResource{} +var _ resource.ResourceWithImportState = &KafkaResource{} + +type KafkaResource struct { + client *core.UniversalClient +} + +// KafkaAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration. +type KafkaAccessConfigurationModel struct { + AllocationExternal types.Bool `tfsdk:"allocation_external" json:"allocationExternal"` + MasterAccessList types.String `tfsdk:"master_access_list" json:"masterAccessList"` +} + +// KafkaClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type KafkaClusterConfigurationModel struct { + Cpu types.Int64 `tfsdk:"cpu" json:"cpu"` + Memory types.Int64 `tfsdk:"memory" json:"memory"` + Replicas types.Int64 `tfsdk:"replicas" json:"replicas"` + Disk types.Int64 `tfsdk:"disk" json:"disk"` +} + +// KafkaStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type KafkaStartupConfigurationModel struct { + ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"` +} + +type KafkaModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessConfiguration *KafkaAccessConfigurationModel `tfsdk:"access_configuration"` + ClusterConfiguration *KafkaClusterConfigurationModel `tfsdk:"cluster_configuration"` + StartupConfiguration *KafkaStartupConfigurationModel `tfsdk:"startup_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewKafkaResource() resource.Resource { + return &KafkaResource{} +} + +func (r *KafkaResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_kafka" +} + +func (r *KafkaResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "allocation_external": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2000), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"}, + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2048)}, + "replicas": schema.Int64Attribute{Required: true}, + "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)}, + }}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8s-4-ext-nubes-ru"), MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"}, + }}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *KafkaResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *KafkaModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *KafkaModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan KafkaModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 1084: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}), + 1085: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64())}), + 1086: resources_core.BuildJSON(map[string]string{"allocationExternal": fmt.Sprintf("%v", config.AccessConfiguration.AllocationExternal.ValueBool()), "masterAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterAccessList.ValueString())}), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 116, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *KafkaResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data KafkaModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 116, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 1084: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}), + 1085: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64())}), + 1086: resources_core.BuildJSON(map[string]string{"allocationExternal": fmt.Sprintf("%v", data.AccessConfiguration.AllocationExternal.ValueBool()), "masterAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterAccessList.ValueString())}), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 116, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 116, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *KafkaResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state KafkaModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 116, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *KafkaResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan KafkaModel + var state KafkaModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 1087: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64())}), + 1088: resources_core.BuildJSON(map[string]string{"allocationExternal": fmt.Sprintf("%v", plan.AccessConfiguration.AllocationExternal.ValueBool()), "masterAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterAccessList.ValueString())}), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 116, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *KafkaResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state KafkaModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *KafkaResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *KafkaResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/116_kafka_topic_resource.go b/generated/prod/go/116_kafka_topic_resource.go new file mode 100644 index 0000000..3f289b3 --- /dev/null +++ b/generated/prod/go/116_kafka_topic_resource.go @@ -0,0 +1,364 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: kafka +// Subresource: topic + +var _ resource.Resource = &KafkaTopicResource{} + +// KafkaTopicResource manages a subresource via operations. +type KafkaTopicResource struct { + client *core.UniversalClient +} + +type KafkaTopicModel struct { + ID types.String `tfsdk:"id"` + KafkaID types.String `tfsdk:"kafka_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + NameTopic types.String `tfsdk:"name_topic"` + Partitions types.Int64 `tfsdk:"partitions"` + Replicas types.Int64 `tfsdk:"replicas"` +} + +func NewKafkaTopicResource() resource.Resource { + return &KafkaTopicResource{} +} + +func (r *KafkaTopicResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_kafka_topic" +} + +func (r *KafkaTopicResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "kafka_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "name_topic": schema.StringAttribute{Required: true, MarkdownDescription: "Пример: `orders-events` Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "partitions": schema.Int64Attribute{Required: true, MarkdownDescription: "Число партиций можно увеличить, но уменьшить их нельзя."}, + "replicas": schema.Int64Attribute{Required: true, MarkdownDescription: "Количество реплик не может привышать количество реплик брокера"}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *KafkaTopicResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan KafkaTopicModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if plan.NameTopic.IsNull() || plan.NameTopic.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "name_topic is required.") + return + } + if plan.Partitions.IsNull() || plan.Partitions.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "partitions is required.") + return + } + if plan.Replicas.IsNull() || plan.Replicas.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "replicas is required.") + return + } + + instanceUID := strings.TrimSpace(plan.KafkaID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "nameTopic": resources_core.FormatString(plan.NameTopic), + } + identityCodes := []string{ + "nameTopic", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("topic", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "topic", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "nameTopic": resources_core.FormatString(plan.NameTopic), + "partitions": resources_core.FormatInt64(plan.Partitions), + "replicas": resources_core.FormatInt64(plan.Replicas), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_topic", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_topic", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "topic", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "topic", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "topic", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *KafkaTopicResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state KafkaTopicModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *KafkaTopicResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan KafkaTopicModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "modify_topic" == "" { + var state KafkaTopicModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.KafkaID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{ + "nameTopic": resources_core.FormatString(plan.NameTopic), + "partitions": resources_core.FormatInt64(plan.Partitions), + "replicas": resources_core.FormatInt64(plan.Replicas), + }) + + idParams := map[string]string{ + "nameTopic": resources_core.FormatString(plan.NameTopic), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "modify_topic", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "topic", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *KafkaTopicResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state KafkaTopicModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_topic" == "" { + return + } + + instanceUID := strings.TrimSpace(state.KafkaID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "nameTopic": resources_core.FormatString(state.NameTopic), + } + identityCodes := []string{ + "nameTopic", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("topic", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "nameTopic": resources_core.FormatString(state.NameTopic), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_topic", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *KafkaTopicResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/116_kafka_user_resource.go b/generated/prod/go/116_kafka_user_resource.go new file mode 100644 index 0000000..52ea73c --- /dev/null +++ b/generated/prod/go/116_kafka_user_resource.go @@ -0,0 +1,373 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: kafka +// Subresource: user + +var _ resource.Resource = &KafkaUserResource{} + +// KafkaUserResource manages a subresource via operations. +type KafkaUserResource struct { + client *core.UniversalClient +} + +type KafkaUserModel struct { + ID types.String `tfsdk:"id"` + KafkaID types.String `tfsdk:"kafka_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessHosts types.String `tfsdk:"access_hosts"` + Group types.String `tfsdk:"group"` + NameTopic types.String `tfsdk:"name_topic"` + Operations types.String `tfsdk:"operations"` + Username types.String `tfsdk:"username"` +} + +func NewKafkaUserResource() resource.Resource { + return &KafkaUserResource{} +} + +func (r *KafkaUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_kafka_user" +} + +func (r *KafkaUserResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "kafka_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_hosts": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("*"), MarkdownDescription: "Пример: `192.168.1.0/24` или `*`"}, + "group": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("*"), MarkdownDescription: "Пример: `order-consumers` или `*`"}, + "name_topic": schema.StringAttribute{Required: true, MarkdownDescription: "Пример: `orders-events`. Использовать маску нельзя(`orders*`), только полное название или `*`"}, + "operations": schema.StringAttribute{Required: true, MarkdownDescription: "Пример: `Create, Describe, Read, Write`"}, + "username": schema.StringAttribute{Required: true, MarkdownDescription: "Пример: `username-admin` Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *KafkaUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan KafkaUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if plan.NameTopic.IsNull() || plan.NameTopic.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "name_topic is required.") + return + } + if plan.Operations.IsNull() || plan.Operations.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "operations is required.") + return + } + if plan.Username.IsNull() || plan.Username.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "username is required.") + return + } + + instanceUID := strings.TrimSpace(plan.KafkaID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "username": resources_core.FormatString(plan.Username), + } + identityCodes := []string{ + "username", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "username": resources_core.FormatString(plan.Username), + "nameTopic": resources_core.FormatString(plan.NameTopic), + "operations": resources_core.FormatString(plan.Operations), + "group": resources_core.FormatString(plan.Group), + "accessHosts": resources_core.FormatString(plan.AccessHosts), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *KafkaUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state KafkaUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *KafkaUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan KafkaUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "modify_user" == "" { + var state KafkaUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.KafkaID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{ + "nameTopic": resources_core.FormatString(plan.NameTopic), + "operations": resources_core.FormatString(plan.Operations), + "group": resources_core.FormatString(plan.Group), + "accessHosts": resources_core.FormatString(plan.AccessHosts), + "username": resources_core.FormatString(plan.Username), + }) + + idParams := map[string]string{ + "username": resources_core.FormatString(plan.Username), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "modify_user", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *KafkaUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state KafkaUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_user" == "" { + return + } + + instanceUID := strings.TrimSpace(state.KafkaID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "username": resources_core.FormatString(state.Username), + } + identityCodes := []string{ + "username", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "username": resources_core.FormatString(state.Username), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_user", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *KafkaUserResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/117_nifi_resource.go b/generated/prod/go/117_nifi_resource.go new file mode 100644 index 0000000..77f570f --- /dev/null +++ b/generated/prod/go/117_nifi_resource.go @@ -0,0 +1,533 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: nifi +// Service ID: 117 + +var _ resource.Resource = &NifiResource{} +var _ resource.ResourceWithModifyPlan = &NifiResource{} +var _ resource.ResourceWithImportState = &NifiResource{} + +type NifiResource struct { + client *core.UniversalClient +} + +type NifiModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + KafkaUid types.String `tfsdk:"kafka_uid"` + NameTopic types.String `tfsdk:"name_topic"` + Partitions types.Int64 `tfsdk:"partitions"` + Replicas types.Int64 `tfsdk:"replicas"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewNifiResource() resource.Resource { + return &NifiResource{} +} + +func (r *NifiResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_nifi" +} + +func (r *NifiResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "kafka_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "Выбрать кластер из списка Не изменяется после создания."}, + "name_topic": schema.StringAttribute{Required: true, MarkdownDescription: "Пример: `orders-events` Не изменяется после создания."}, + "partitions": schema.Int64Attribute{Required: true, MarkdownDescription: "Число партиций на кластер ≤ 100 × (число брокеров) × (фактор репликации). Пример `50`"}, + "replicas": schema.Int64Attribute{Required: true, MarkdownDescription: "Количество партиций ≤ (число брокеров). Пример `2`"}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *NifiResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *NifiModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *NifiModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan NifiModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan. + // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов. + // Resolve (displayName → UUID или uppercase → lowercase) нужен только для + // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка + // "Provider produced invalid plan: planned value does not match config value". + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.KafkaUid.IsNull() && !plan.KafkaUid.IsUnknown() && !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.KafkaUid.ValueString(), state.KafkaUid.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить kafka_uid", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.NameTopic.IsNull() && !plan.NameTopic.IsUnknown() && !state.NameTopic.IsNull() && !state.NameTopic.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.NameTopic.ValueString(), state.NameTopic.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить name_topic", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.KafkaUid.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "kafka_uid is required.") + return + } + if config.KafkaUid.IsUnknown() { + return + } + if config.NameTopic.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "name_topic is required.") + return + } + if config.NameTopic.IsUnknown() { + return + } + if config.Partitions.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "partitions is required.") + return + } + if config.Partitions.IsUnknown() { + return + } + if config.Replicas.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "replicas is required.") + return + } + if config.Replicas.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + if !config.KafkaUid.IsNull() && !config.KafkaUid.IsUnknown() { + resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, config.KafkaUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve kafka_uid", err.Error()) + } else if resolvedKafkaUid != "" && resolvedKafkaUid != config.KafkaUid.ValueString() { + config.KafkaUid = types.StringValue(resolvedKafkaUid) + } + } + params := map[int]string{ + 471: resources_core.FormatString(config.KafkaUid), + 472: resources_core.FormatInt64(config.Partitions), + 473: resources_core.FormatInt64(config.Replicas), + 474: resources_core.FormatString(config.NameTopic), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 117, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *NifiResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data NifiModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.KafkaUid.IsNull() || data.KafkaUid.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "kafka_uid is required.") + return + } + if data.NameTopic.IsNull() || data.NameTopic.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "name_topic is required.") + return + } + if data.Partitions.IsNull() || data.Partitions.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "partitions is required.") + return + } + if data.Replicas.IsNull() || data.Replicas.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "replicas is required.") + return + } + // ══════════════════════════════════════════════════════════════════════════ + // ПРАВИЛО TERRAFORM (официальная документация): + // «If an attribute value is configured, it is NEVER valid to change that + // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что + // написал пользователь). Менять plan запрещено на уровне фреймворка. + // + // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре. + // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный). + // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит: + // «Provider produced inconsistent result after apply». + // + // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот. + // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как + // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API). + // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный + // регистр в state через strings.EqualFold (сравниваем без учёта регистра). + // ══════════════════════════════════════════════════════════════════════════ + originalKafkaUid := data.KafkaUid + if !data.KafkaUid.IsNull() && !data.KafkaUid.IsUnknown() { + resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, data.KafkaUid.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if resolvedKafkaUid != "" && resolvedKafkaUid != data.KafkaUid.ValueString() { + data.KafkaUid = types.StringValue(resolvedKafkaUid) + } + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 117, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 471: resources_core.FormatString(data.KafkaUid), + 472: resources_core.FormatInt64(data.Partitions), + 473: resources_core.FormatInt64(data.Replicas), + 474: resources_core.FormatString(data.NameTopic), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 117, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 117, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "kafkaUid", Field: "KafkaUid", Type: "string"}, + {Code: "nameTopic", Field: "NameTopic", Type: "string"}, + {Code: "partitions", Field: "Partitions", Type: "int64"}, + {Code: "replicas", Field: "Replicas", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() { + resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, state.KafkaUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve kafka_uid", err.Error()) + } else if resolvedKafkaUid != "" && resolvedKafkaUid != state.KafkaUid.ValueString() { + state.KafkaUid = types.StringValue(resolvedKafkaUid) + } + } + // Restore user-provided casing в state (Create). + // EqualFold = «совпадают ли значения без учёта регистра?» + // Если да — значит API вернул «туже» строку, только в другом регистре. + // Заменяем state на original (то что было в plan/config пользователя). + // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint. + if !originalKafkaUid.IsNull() && !originalKafkaUid.IsUnknown() && !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() { + if strings.EqualFold(state.KafkaUid.ValueString(), originalKafkaUid.ValueString()) { + state.KafkaUid = originalKafkaUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *NifiResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state NifiModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 117, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "kafkaUid", Field: "KafkaUid", Type: "string"}, + {Code: "nameTopic", Field: "NameTopic", Type: "string"}, + {Code: "partitions", Field: "Partitions", Type: "int64"}, + {Code: "replicas", Field: "Replicas", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !newState.KafkaUid.IsNull() && !newState.KafkaUid.IsUnknown() { + resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, newState.KafkaUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve kafka_uid", err.Error()) + } else if resolvedKafkaUid != "" && resolvedKafkaUid != newState.KafkaUid.ValueString() { + newState.KafkaUid = types.StringValue(resolvedKafkaUid) + } + } + // Restore user-provided casing в state (Read). + // При чтении у нас нет plan — но предыдущий state уже хранит значение + // в регистре пользователя (после первого Create оно было восстановлено). + // Берём prior state (переменная state) как эталон регистра. + // Если API вернул то же UUID только строчными буквами — восстанавливаем. + if !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() && !newState.KafkaUid.IsNull() && !newState.KafkaUid.IsUnknown() { + if strings.EqualFold(newState.KafkaUid.ValueString(), state.KafkaUid.ValueString()) { + newState.KafkaUid = state.KafkaUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *NifiResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan NifiModel + var state NifiModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.Partitions.IsNull() != state.Partitions.IsNull() || plan.Partitions.IsUnknown() != state.Partitions.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.Partitions.IsNull() && !plan.Partitions.IsUnknown() && !state.Partitions.IsNull() && !state.Partitions.IsUnknown() { + if plan.Partitions.ValueInt64() != state.Partitions.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.Replicas.IsNull() != state.Replicas.IsNull() || plan.Replicas.IsUnknown() != state.Replicas.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.Replicas.IsNull() && !plan.Replicas.IsUnknown() && !state.Replicas.IsNull() && !state.Replicas.IsUnknown() { + if plan.Replicas.ValueInt64() != state.Replicas.ValueInt64() { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 477: resources_core.FormatInt64(plan.Partitions), + 478: resources_core.FormatInt64(plan.Replicas), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 117, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "kafkaUid", Field: "KafkaUid", Type: "string"}, + {Code: "nameTopic", Field: "NameTopic", Type: "string"}, + {Code: "partitions", Field: "Partitions", Type: "int64"}, + {Code: "replicas", Field: "Replicas", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() { + resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, state.KafkaUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve kafka_uid", err.Error()) + } else if resolvedKafkaUid != "" && resolvedKafkaUid != state.KafkaUid.ValueString() { + state.KafkaUid = types.StringValue(resolvedKafkaUid) + } + } + // Restore user-provided casing в state (Update). + // После API-вызова modify state содержит значения в lower-case от API. + // Plan == config == то что написал пользователь (регистр неизменён). + // EqualFold: если UUID совпадает без учёта регистра — берём из plan. + if !plan.KafkaUid.IsNull() && !plan.KafkaUid.IsUnknown() && !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() { + if strings.EqualFold(state.KafkaUid.ValueString(), plan.KafkaUid.ValueString()) { + state.KafkaUid = plan.KafkaUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *NifiResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state NifiModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *NifiResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *NifiResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/119_akhq_resource.go b/generated/prod/go/119_akhq_resource.go new file mode 100644 index 0000000..d7e3c02 --- /dev/null +++ b/generated/prod/go/119_akhq_resource.go @@ -0,0 +1,544 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: akhq +// Service ID: 119 + +var _ resource.Resource = &AkhqResource{} +var _ resource.ResourceWithModifyPlan = &AkhqResource{} +var _ resource.ResourceWithImportState = &AkhqResource{} + +type AkhqResource struct { + client *core.UniversalClient +} + +type AkhqModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + Domain types.String `tfsdk:"domain"` + KafkaUid types.String `tfsdk:"kafka_uid"` + ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"` + ResourceInstances types.Int64 `tfsdk:"resource_instances"` + ResourceMemory types.Int64 `tfsdk:"resource_memory"` + ResourceRealm types.String `tfsdk:"resource_realm"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewAkhqResource() resource.Resource { + return &AkhqResource{} +} + +func (r *AkhqResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_akhq" +} + +func (r *AkhqResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "domain": schema.StringAttribute{Required: true, MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."}, + "kafka_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "Выбрать кластер из списка"}, + "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Пример: `1000` Не изменяется после создания."}, + "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Не изменяется после создания."}, + "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Пример: `1024` Не изменяется после создания."}, + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8s-4-ext-nubes-ru"), MarkdownDescription: "Выбрать платформу из списка Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *AkhqResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *AkhqModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *AkhqModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan AkhqModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan. + // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов. + // Resolve (displayName → UUID или uppercase → lowercase) нужен только для + // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка + // "Provider produced invalid plan: planned value does not match config value". + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() { + if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_c_p_u", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() { + if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_instances", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() { + if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_memory", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.Domain.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "domain is required.") + return + } + if config.Domain.IsUnknown() { + return + } + if config.KafkaUid.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "kafka_uid is required.") + return + } + if config.KafkaUid.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + if !config.KafkaUid.IsNull() && !config.KafkaUid.IsUnknown() { + resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, config.KafkaUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve kafka_uid", err.Error()) + } else if resolvedKafkaUid != "" && resolvedKafkaUid != config.KafkaUid.ValueString() { + config.KafkaUid = types.StringValue(resolvedKafkaUid) + } + } + params := map[int]string{ + 550: resources_core.FormatString(config.ResourceRealm), + 551: resources_core.FormatInt64(config.ResourceMemory), + 552: resources_core.FormatInt64(config.ResourceCPU), + 553: resources_core.FormatString(config.Domain), + 554: resources_core.FormatInt64(config.ResourceInstances), + 555: resources_core.FormatString(config.KafkaUid), + } + desiredDomain := "" + if !config.Domain.IsNull() && !config.Domain.IsUnknown() { + desiredDomain = config.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 119, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *AkhqResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data AkhqModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.Domain.IsNull() || data.Domain.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "domain is required.") + return + } + if data.KafkaUid.IsNull() || data.KafkaUid.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "kafka_uid is required.") + return + } + // ══════════════════════════════════════════════════════════════════════════ + // ПРАВИЛО TERRAFORM (официальная документация): + // «If an attribute value is configured, it is NEVER valid to change that + // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что + // написал пользователь). Менять plan запрещено на уровне фреймворка. + // + // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре. + // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный). + // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит: + // «Provider produced inconsistent result after apply». + // + // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот. + // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как + // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API). + // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный + // регистр в state через strings.EqualFold (сравниваем без учёта регистра). + // ══════════════════════════════════════════════════════════════════════════ + originalKafkaUid := data.KafkaUid + if !data.KafkaUid.IsNull() && !data.KafkaUid.IsUnknown() { + resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, data.KafkaUid.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if resolvedKafkaUid != "" && resolvedKafkaUid != data.KafkaUid.ValueString() { + data.KafkaUid = types.StringValue(resolvedKafkaUid) + } + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + if !data.Domain.IsNull() && !data.Domain.IsUnknown() { + desiredDomain = data.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 119, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 550: resources_core.FormatString(data.ResourceRealm), + 551: resources_core.FormatInt64(data.ResourceMemory), + 552: resources_core.FormatInt64(data.ResourceCPU), + 553: resources_core.FormatString(data.Domain), + 554: resources_core.FormatInt64(data.ResourceInstances), + 555: resources_core.FormatString(data.KafkaUid), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 119, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 119, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "kafkaUid", Field: "KafkaUid", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() { + resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, state.KafkaUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve kafka_uid", err.Error()) + } else if resolvedKafkaUid != "" && resolvedKafkaUid != state.KafkaUid.ValueString() { + state.KafkaUid = types.StringValue(resolvedKafkaUid) + } + } + // Restore user-provided casing в state (Create). + // EqualFold = «совпадают ли значения без учёта регистра?» + // Если да — значит API вернул «туже» строку, только в другом регистре. + // Заменяем state на original (то что было в plan/config пользователя). + // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint. + if !originalKafkaUid.IsNull() && !originalKafkaUid.IsUnknown() && !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() { + if strings.EqualFold(state.KafkaUid.ValueString(), originalKafkaUid.ValueString()) { + state.KafkaUid = originalKafkaUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *AkhqResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state AkhqModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 119, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "kafkaUid", Field: "KafkaUid", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !newState.KafkaUid.IsNull() && !newState.KafkaUid.IsUnknown() { + resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, newState.KafkaUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve kafka_uid", err.Error()) + } else if resolvedKafkaUid != "" && resolvedKafkaUid != newState.KafkaUid.ValueString() { + newState.KafkaUid = types.StringValue(resolvedKafkaUid) + } + } + // Restore user-provided casing в state (Read). + // При чтении у нас нет plan — но предыдущий state уже хранит значение + // в регистре пользователя (после первого Create оно было восстановлено). + // Берём prior state (переменная state) как эталон регистра. + // Если API вернул то же UUID только строчными буквами — восстанавливаем. + if !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() && !newState.KafkaUid.IsNull() && !newState.KafkaUid.IsUnknown() { + if strings.EqualFold(newState.KafkaUid.ValueString(), state.KafkaUid.ValueString()) { + newState.KafkaUid = state.KafkaUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *AkhqResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan AkhqModel + var state AkhqModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.KafkaUid.IsNull() != state.KafkaUid.IsNull() || plan.KafkaUid.IsUnknown() != state.KafkaUid.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.KafkaUid.IsNull() && !plan.KafkaUid.IsUnknown() && !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() { + if !strings.EqualFold(plan.KafkaUid.ValueString(), state.KafkaUid.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 565: resources_core.FormatString(plan.KafkaUid), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 119, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "kafkaUid", Field: "KafkaUid", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() { + resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, state.KafkaUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve kafka_uid", err.Error()) + } else if resolvedKafkaUid != "" && resolvedKafkaUid != state.KafkaUid.ValueString() { + state.KafkaUid = types.StringValue(resolvedKafkaUid) + } + } + // Restore user-provided casing в state (Update). + // После API-вызова modify state содержит значения в lower-case от API. + // Plan == config == то что написал пользователь (регистр неизменён). + // EqualFold: если UUID совпадает без учёта регистра — берём из plan. + if !plan.KafkaUid.IsNull() && !plan.KafkaUid.IsUnknown() && !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() { + if strings.EqualFold(state.KafkaUid.ValueString(), plan.KafkaUid.ValueString()) { + state.KafkaUid = plan.KafkaUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *AkhqResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state AkhqModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *AkhqResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *AkhqResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/120_clickhouse_database_resource.go b/generated/prod/go/120_clickhouse_database_resource.go new file mode 100644 index 0000000..cc5c42e --- /dev/null +++ b/generated/prod/go/120_clickhouse_database_resource.go @@ -0,0 +1,343 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: clickhouse +// Subresource: database + +var _ resource.Resource = &ClickhouseDatabaseResource{} + +// ClickhouseDatabaseResource manages a subresource via operations. +type ClickhouseDatabaseResource struct { + client *core.UniversalClient +} + +type ClickhouseDatabaseModel struct { + ID types.String `tfsdk:"id"` + ClickhouseID types.String `tfsdk:"clickhouse_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + DbName types.String `tfsdk:"db_name"` +} + +func NewClickhouseDatabaseResource() resource.Resource { + return &ClickhouseDatabaseResource{} +} + +func (r *ClickhouseDatabaseResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_clickhouse_database" +} + +func (r *ClickhouseDatabaseResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "clickhouse_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "db_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("db01"), MarkdownDescription: "Пример: `db01` Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *ClickhouseDatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan ClickhouseDatabaseModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.ClickhouseID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "dbName": resources_core.FormatString(plan.DbName), + } + identityCodes := []string{ + "dbName", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("database", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "dbName": resources_core.FormatString(plan.DbName), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_database", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_database", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *ClickhouseDatabaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state ClickhouseDatabaseModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *ClickhouseDatabaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan ClickhouseDatabaseModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state ClickhouseDatabaseModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.ClickhouseID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{ + "dbName": resources_core.FormatString(plan.DbName), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *ClickhouseDatabaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state ClickhouseDatabaseModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_database" == "" { + return + } + + instanceUID := strings.TrimSpace(state.ClickhouseID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "dbName": resources_core.FormatString(state.DbName), + } + identityCodes := []string{ + "dbName", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("database", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "dbName": resources_core.FormatString(state.DbName), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_database", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *ClickhouseDatabaseResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/120_clickhouse_resource.go b/generated/prod/go/120_clickhouse_resource.go new file mode 100644 index 0000000..53dcba4 --- /dev/null +++ b/generated/prod/go/120_clickhouse_resource.go @@ -0,0 +1,487 @@ +package resources_gen + +import ( + "context" + "fmt" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: clickhouse +// Service ID: 120 + +var _ resource.Resource = &ClickhouseResource{} +var _ resource.ResourceWithModifyPlan = &ClickhouseResource{} +var _ resource.ResourceWithImportState = &ClickhouseResource{} + +type ClickhouseResource struct { + client *core.UniversalClient +} + +// ClickhouseAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration. +type ClickhouseAccessConfigurationModel struct { + MasterIpSpace types.String `tfsdk:"master_ip_space" json:"masterIpSpace"` + MasterAccessList types.String `tfsdk:"master_access_list" json:"masterAccessList"` +} + +// ClickhouseAutoscaleConfigurationModel — вложенная модель для map-fixed параметра autoscaleConfiguration. +type ClickhouseAutoscaleConfigurationModel struct { + Enabled types.Bool `tfsdk:"enabled" json:"enabled"` + Schedule types.Int64 `tfsdk:"schedule" json:"schedule"` + Quota types.Int64 `tfsdk:"quota" json:"quota"` + Percent types.Int64 `tfsdk:"percent" json:"percent"` +} + +// ClickhouseBackupConfigurationModel — вложенная модель для map-fixed параметра backupConfiguration. +type ClickhouseBackupConfigurationModel struct { + S3Uid types.String `tfsdk:"s3_uid" json:"s3Uid"` + Retain types.Int64 `tfsdk:"retain" json:"retain"` + Schedule types.String `tfsdk:"schedule" json:"schedule"` +} + +// ClickhouseClickhouseConfigurationModel — вложенная модель для map-fixed параметра clickhouseConfiguration. +type ClickhouseClickhouseConfigurationModel struct { + Version types.String `tfsdk:"version" json:"version"` +} + +// ClickhouseClickhouseKeeperConfigurationModel — вложенная модель для map-fixed параметра clickhouseKeeperConfiguration. +type ClickhouseClickhouseKeeperConfigurationModel struct { + Cpu types.Int64 `tfsdk:"cpu" json:"cpu"` + Memory types.Int64 `tfsdk:"memory" json:"memory"` + Replicas types.Int64 `tfsdk:"replicas" json:"replicas"` +} + +// ClickhouseClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type ClickhouseClusterConfigurationModel struct { + Cpu types.Int64 `tfsdk:"cpu" json:"cpu"` + Memory types.Int64 `tfsdk:"memory" json:"memory"` + Replicas types.Int64 `tfsdk:"replicas" json:"replicas"` + Shards types.Int64 `tfsdk:"shards" json:"shards"` + Disk types.Int64 `tfsdk:"disk" json:"disk"` +} + +// ClickhouseStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type ClickhouseStartupConfigurationModel struct { + ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"` +} + +type ClickhouseModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessConfiguration *ClickhouseAccessConfigurationModel `tfsdk:"access_configuration"` + AutoscaleConfiguration *ClickhouseAutoscaleConfigurationModel `tfsdk:"autoscale_configuration"` + BackupConfiguration *ClickhouseBackupConfigurationModel `tfsdk:"backup_configuration"` + ClickhouseConfiguration *ClickhouseClickhouseConfigurationModel `tfsdk:"clickhouse_configuration"` + ClickhouseFilesConfiguration types.String `tfsdk:"clickhouse_files_configuration"` + ClickhouseKeeperConfiguration *ClickhouseClickhouseKeeperConfigurationModel `tfsdk:"clickhouse_keeper_configuration"` + ClusterConfiguration *ClickhouseClusterConfigurationModel `tfsdk:"cluster_configuration"` + StartupConfiguration *ClickhouseStartupConfigurationModel `tfsdk:"startup_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewClickhouseResource() resource.Resource { + return &ClickhouseResource{} +} + +func (r *ClickhouseResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_clickhouse" +} + +func (r *ClickhouseResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "master_ip_space": schema.StringAttribute{Required: true, MarkdownDescription: "Из какого Ip-Space резервировать внешний IP
Список предоставляется из Организации, в котором развёрнута ресурсная платформа"}, + "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"}, + }}, + "autoscale_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "enabled": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включает автоскейлинг PV. При включении параметра необходимо настроить autoScalePercentage и autoScaleTechWindow. Алерт при срабатывании вызывает modify у текущей услуги"}, + "schedule": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(0), MarkdownDescription: "Требует включения autoScale. Окно обновления (пока не реализовано). Указывается час в виде Integer (0 / 5 / 12 / 23)"}, + "quota": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100), MarkdownDescription: "До какого размера возможно увеличивать диск, когда включен Autoscale"}, + "percent": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Требует включения autoScale. Позволяет указать в процентах расширение от текущего макс объема
Минимальный размер на который расширяется - **1Gb**"}, + }}, + "backup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "s3_uid": schema.StringAttribute{Required: true, MarkdownDescription: "Экземпляр S3 обязателен"}, + "retain": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(7)}, + "schedule": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("0 0 * * *")}, + }}, + "clickhouse_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("25.10")}, + }}, + "clickhouse_files_configuration": schema.StringAttribute{Optional: true, MarkdownDescription: "Формат:
- path: somethings3.xml
xmlConfig: '<clickhouse>......</clickhouse>'"}, + "clickhouse_keeper_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"}, + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Указывается в Megabytes"}, + "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(0), MarkdownDescription: "Если указан 0, то инстансы ClickHouse Keeper развёрнуты не будут"}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1000), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"}, + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2048), MarkdownDescription: "Указывается в Megabytes"}, + "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)}, + "shards": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "**Шард** - это часть данных, разделенная на разные узлы (серверы) для горизонтального масштабирования и обработки больших объемов информации.
**Реплика** - это точная копия данных внутри одного шарда"}, + "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Указывается в GIGAbytes"}, + }}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8s-4-ext-nubes-ru"), MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"}, + }}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *ClickhouseResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *ClickhouseModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *ClickhouseModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan ClickhouseModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 837: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}), + 838: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "shards": fmt.Sprintf("%d", config.ClusterConfiguration.Shards.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64())}), + 839: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterAccessList.ValueString())}), + 840: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClickhouseKeeperConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClickhouseKeeperConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClickhouseKeeperConfiguration.Replicas.ValueInt64())}), + 841: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.ClickhouseConfiguration.Version.ValueString())}), + 842: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", config.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", config.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", config.BackupConfiguration.Schedule.ValueString())}), + 843: resources_core.BuildJSON(map[string]string{"enabled": fmt.Sprintf("%v", config.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", config.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", config.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", config.AutoscaleConfiguration.Percent.ValueInt64())}), + 851: resources_core.FormatString(config.ClickhouseFilesConfiguration), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 120, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *ClickhouseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data ClickhouseModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 120, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 837: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}), + 838: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "shards": fmt.Sprintf("%d", data.ClusterConfiguration.Shards.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64())}), + 839: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterAccessList.ValueString())}), + 840: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClickhouseKeeperConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClickhouseKeeperConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClickhouseKeeperConfiguration.Replicas.ValueInt64())}), + 841: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.ClickhouseConfiguration.Version.ValueString())}), + 842: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", data.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", data.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", data.BackupConfiguration.Schedule.ValueString())}), + 843: resources_core.BuildJSON(map[string]string{"enabled": fmt.Sprintf("%v", data.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", data.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", data.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", data.AutoscaleConfiguration.Percent.ValueInt64())}), + 851: resources_core.FormatString(data.ClickhouseFilesConfiguration), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 120, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 120, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"}, + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"}, + {Code: "clickhouseConfiguration", Field: "ClickhouseConfiguration", Type: "map-fixed"}, + {Code: "clickhouseFilesConfiguration", Field: "ClickhouseFilesConfiguration", Type: "array-map-fixed"}, + {Code: "clickhouseKeeperConfiguration", Field: "ClickhouseKeeperConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *ClickhouseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state ClickhouseModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 120, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"}, + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"}, + {Code: "clickhouseConfiguration", Field: "ClickhouseConfiguration", Type: "map-fixed"}, + {Code: "clickhouseFilesConfiguration", Field: "ClickhouseFilesConfiguration", Type: "array-map-fixed"}, + {Code: "clickhouseKeeperConfiguration", Field: "ClickhouseKeeperConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *ClickhouseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan ClickhouseModel + var state ClickhouseModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.ClickhouseFilesConfiguration.IsNull() != state.ClickhouseFilesConfiguration.IsNull() || plan.ClickhouseFilesConfiguration.IsUnknown() != state.ClickhouseFilesConfiguration.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ClickhouseFilesConfiguration.IsNull() && !plan.ClickhouseFilesConfiguration.IsUnknown() && !state.ClickhouseFilesConfiguration.IsNull() && !state.ClickhouseFilesConfiguration.IsUnknown() { + if !strings.EqualFold(plan.ClickhouseFilesConfiguration.ValueString(), state.ClickhouseFilesConfiguration.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 844: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "shards": fmt.Sprintf("%d", plan.ClusterConfiguration.Shards.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64())}), + 845: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClickhouseKeeperConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClickhouseKeeperConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClickhouseKeeperConfiguration.Replicas.ValueInt64())}), + 846: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterAccessList.ValueString())}), + 847: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", plan.ClickhouseConfiguration.Version.ValueString())}), + 848: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", plan.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", plan.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", plan.BackupConfiguration.Schedule.ValueString())}), + 849: resources_core.BuildJSON(map[string]string{"quota": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Percent.ValueInt64()), "enabled": fmt.Sprintf("%v", plan.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Schedule.ValueInt64())}), + 850: resources_core.FormatString(plan.ClickhouseFilesConfiguration), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 120, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"}, + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"}, + {Code: "clickhouseConfiguration", Field: "ClickhouseConfiguration", Type: "map-fixed"}, + {Code: "clickhouseFilesConfiguration", Field: "ClickhouseFilesConfiguration", Type: "array-map-fixed"}, + {Code: "clickhouseKeeperConfiguration", Field: "ClickhouseKeeperConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *ClickhouseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state ClickhouseModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *ClickhouseResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *ClickhouseResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/120_clickhouse_user_resource.go b/generated/prod/go/120_clickhouse_user_resource.go new file mode 100644 index 0000000..8dc28f7 --- /dev/null +++ b/generated/prod/go/120_clickhouse_user_resource.go @@ -0,0 +1,359 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: clickhouse +// Subresource: user + +var _ resource.Resource = &ClickhouseUserResource{} + +// ClickhouseUserResource manages a subresource via operations. +type ClickhouseUserResource struct { + client *core.UniversalClient +} + +type ClickhouseUserModel struct { + ID types.String `tfsdk:"id"` + ClickhouseID types.String `tfsdk:"clickhouse_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessHosts types.String `tfsdk:"access_hosts"` + DbName types.String `tfsdk:"db_name"` + Username types.String `tfsdk:"username"` +} + +func NewClickhouseUserResource() resource.Resource { + return &ClickhouseUserResource{} +} + +func (r *ClickhouseUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_clickhouse_user" +} + +func (r *ClickhouseUserResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "clickhouse_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_hosts": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("::/0"), MarkdownDescription: "Пример: `::/0` Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "db_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "username": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("myusername"), MarkdownDescription: "Пример: `myusername` Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *ClickhouseUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan ClickhouseUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.ClickhouseID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "accessHosts": resources_core.FormatString(plan.AccessHosts), + "dbName": resources_core.FormatString(plan.DbName), + "username": resources_core.FormatString(plan.Username), + } + identityCodes := []string{ + "accessHosts", + "dbName", + "username", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "username": resources_core.FormatString(plan.Username), + "dbName": resources_core.FormatString(plan.DbName), + "accessHosts": resources_core.FormatString(plan.AccessHosts), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *ClickhouseUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state ClickhouseUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *ClickhouseUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan ClickhouseUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state ClickhouseUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.ClickhouseID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{ + "accessHosts": resources_core.FormatString(plan.AccessHosts), + "dbName": resources_core.FormatString(plan.DbName), + "username": resources_core.FormatString(plan.Username), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *ClickhouseUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state ClickhouseUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_user" == "" { + return + } + + instanceUID := strings.TrimSpace(state.ClickhouseID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "accessHosts": resources_core.FormatString(state.AccessHosts), + "dbName": resources_core.FormatString(state.DbName), + "username": resources_core.FormatString(state.Username), + } + identityCodes := []string{ + "accessHosts", + "dbName", + "username", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "username": resources_core.FormatString(state.Username), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_user", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *ClickhouseUserResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/12_s3_resource.go b/generated/prod/go/12_s3_resource.go new file mode 100644 index 0000000..6347765 --- /dev/null +++ b/generated/prod/go/12_s3_resource.go @@ -0,0 +1,421 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: s3 +// Service ID: 12 + +var _ resource.Resource = &S3Resource{} +var _ resource.ResourceWithModifyPlan = &S3Resource{} +var _ resource.ResourceWithImportState = &S3Resource{} + +type S3Resource struct { + client *core.UniversalClient +} + +type S3Model struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + DisplayName types.String `tfsdk:"display_name"` + MaxBucketsPerUser types.Int64 `tfsdk:"max_buckets_per_user"` + MaxObjectsPerBucket types.Int64 `tfsdk:"max_objects_per_bucket"` + MaxSizeGbPerUser types.Int64 `tfsdk:"max_size_gb_per_user"` + ResourceRealm types.String `tfsdk:"resource_realm"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewS3Resource() resource.Resource { + return &S3Resource{} +} + +func (r *S3Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_s3" +} + +func (r *S3Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "display_name": schema.StringAttribute{Required: true, MarkdownDescription: "Человеко-читаемое имя (транслит из Elma) - передаётся имя клиента Не изменяется после создания."}, + "max_buckets_per_user": schema.Int64Attribute{Optional: true, MarkdownDescription: "Квота на пользователя. Количество бакетов пользователя. Передаём руками"}, + "max_objects_per_bucket": schema.Int64Attribute{Optional: true, MarkdownDescription: "Квота на бакет. Кол-во объектов. Передаём руками"}, + "max_size_gb_per_user": schema.Int64Attribute{Optional: true, MarkdownDescription: "maxSizeGb. Передаём руками"}, + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("s3.msk-1.ngcloud.ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *S3Resource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *S3Model + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *S3Model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan S3Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.DisplayName.IsNull() && !plan.DisplayName.IsUnknown() && !state.DisplayName.IsNull() && !state.DisplayName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.DisplayName.ValueString(), state.DisplayName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить display_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.DisplayName.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "display_name is required.") + return + } + if config.DisplayName.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 163: resources_core.FormatString(config.ResourceRealm), + 320: resources_core.FormatString(config.DisplayName), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 12, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *S3Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data S3Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.DisplayName.IsNull() || data.DisplayName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "display_name is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 12, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 163: resources_core.FormatString(data.ResourceRealm), + 320: resources_core.FormatString(data.DisplayName), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 12, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 12, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "displayName", Field: "DisplayName", Type: "string"}, + {Code: "maxBucketsPerUser", Field: "MaxBucketsPerUser", Type: "int64"}, + {Code: "maxObjectsPerBucket", Field: "MaxObjectsPerBucket", Type: "int64"}, + {Code: "maxSizeGbPerUser", Field: "MaxSizeGbPerUser", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *S3Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state S3Model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 12, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "displayName", Field: "DisplayName", Type: "string"}, + {Code: "maxBucketsPerUser", Field: "MaxBucketsPerUser", Type: "int64"}, + {Code: "maxObjectsPerBucket", Field: "MaxObjectsPerBucket", Type: "int64"}, + {Code: "maxSizeGbPerUser", Field: "MaxSizeGbPerUser", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *S3Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan S3Model + var state S3Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.MaxSizeGbPerUser.IsNull() != state.MaxSizeGbPerUser.IsNull() || plan.MaxSizeGbPerUser.IsUnknown() != state.MaxSizeGbPerUser.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.MaxSizeGbPerUser.IsNull() && !plan.MaxSizeGbPerUser.IsUnknown() && !state.MaxSizeGbPerUser.IsNull() && !state.MaxSizeGbPerUser.IsUnknown() { + if plan.MaxSizeGbPerUser.ValueInt64() != state.MaxSizeGbPerUser.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.MaxObjectsPerBucket.IsNull() != state.MaxObjectsPerBucket.IsNull() || plan.MaxObjectsPerBucket.IsUnknown() != state.MaxObjectsPerBucket.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.MaxObjectsPerBucket.IsNull() && !plan.MaxObjectsPerBucket.IsUnknown() && !state.MaxObjectsPerBucket.IsNull() && !state.MaxObjectsPerBucket.IsUnknown() { + if plan.MaxObjectsPerBucket.ValueInt64() != state.MaxObjectsPerBucket.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.MaxBucketsPerUser.IsNull() != state.MaxBucketsPerUser.IsNull() || plan.MaxBucketsPerUser.IsUnknown() != state.MaxBucketsPerUser.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.MaxBucketsPerUser.IsNull() && !plan.MaxBucketsPerUser.IsUnknown() && !state.MaxBucketsPerUser.IsNull() && !state.MaxBucketsPerUser.IsUnknown() { + if plan.MaxBucketsPerUser.ValueInt64() != state.MaxBucketsPerUser.ValueInt64() { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 50: resources_core.FormatInt64(plan.MaxSizeGbPerUser), + 51: resources_core.FormatInt64(plan.MaxObjectsPerBucket), + 52: resources_core.FormatInt64(plan.MaxBucketsPerUser), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 12, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "displayName", Field: "DisplayName", Type: "string"}, + {Code: "maxBucketsPerUser", Field: "MaxBucketsPerUser", Type: "int64"}, + {Code: "maxObjectsPerBucket", Field: "MaxObjectsPerBucket", Type: "int64"}, + {Code: "maxSizeGbPerUser", Field: "MaxSizeGbPerUser", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *S3Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state S3Model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *S3Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *S3Resource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/12_s3_sub_user_resource.go b/generated/prod/go/12_s3_sub_user_resource.go new file mode 100644 index 0000000..b8e0168 --- /dev/null +++ b/generated/prod/go/12_s3_sub_user_resource.go @@ -0,0 +1,358 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: s3 +// Subresource: sub_user + +var _ resource.Resource = &S3SubUserResource{} + +// S3SubUserResource manages a subresource via operations. +type S3SubUserResource struct { + client *core.UniversalClient +} + +type S3SubUserModel struct { + ID types.String `tfsdk:"id"` + S3ID types.String `tfsdk:"s3_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + SubUserAccess types.String `tfsdk:"sub_user_access"` + SubUserName types.String `tfsdk:"sub_user_name"` +} + +func NewS3SubUserResource() resource.Resource { + return &S3SubUserResource{} +} + +func (r *S3SubUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_s3_sub_user" +} + +func (r *S3SubUserResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "s3_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "sub_user_access": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "sub_user_name": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *S3SubUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan S3SubUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if plan.SubUserAccess.IsNull() || plan.SubUserAccess.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "sub_user_access is required.") + return + } + if plan.SubUserName.IsNull() || plan.SubUserName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "sub_user_name is required.") + return + } + + instanceUID := strings.TrimSpace(plan.S3ID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "subUserAccess": resources_core.FormatString(plan.SubUserAccess), + "subUserName": resources_core.FormatString(plan.SubUserName), + } + identityCodes := []string{ + "subUserAccess", + "subUserName", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("sub_user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "sub_user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "subUserName": resources_core.FormatString(plan.SubUserName), + "subUserAccess": resources_core.FormatString(plan.SubUserAccess), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_sub_user", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_sub_user", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "sub_user", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "sub_user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "sub_user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *S3SubUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state S3SubUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *S3SubUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan S3SubUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state S3SubUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.S3ID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{ + "subUserAccess": resources_core.FormatString(plan.SubUserAccess), + "subUserName": resources_core.FormatString(plan.SubUserName), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "sub_user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *S3SubUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state S3SubUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_sub_user" == "" { + return + } + + instanceUID := strings.TrimSpace(state.S3ID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "subUserAccess": resources_core.FormatString(state.SubUserAccess), + "subUserName": resources_core.FormatString(state.SubUserName), + } + identityCodes := []string{ + "subUserAccess", + "subUserName", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("sub_user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "subUserName": resources_core.FormatString(state.SubUserName), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_sub_user", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *S3SubUserResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/13_s3bucket_resource.go b/generated/prod/go/13_s3bucket_resource.go new file mode 100644 index 0000000..c45905c --- /dev/null +++ b/generated/prod/go/13_s3bucket_resource.go @@ -0,0 +1,478 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: s3bucket +// Service ID: 13 + +var _ resource.Resource = &S3bucketResource{} +var _ resource.ResourceWithModifyPlan = &S3bucketResource{} +var _ resource.ResourceWithImportState = &S3bucketResource{} + +type S3bucketResource struct { + client *core.UniversalClient +} + +type S3bucketModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + BucketName types.String `tfsdk:"bucket_name"` + CorsAll types.Bool `tfsdk:"cors_all"` + ListAll types.Bool `tfsdk:"list_all"` + MaxSize types.String `tfsdk:"max_size"` + Placement types.String `tfsdk:"placement"` + ReadAll types.Bool `tfsdk:"read_all"` + S3UserUid types.String `tfsdk:"s3_user_uid"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewS3bucketResource() resource.Resource { + return &S3bucketResource{} +} + +func (r *S3bucketResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_s3bucket" +} + +func (r *S3bucketResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "bucket_name": schema.StringAttribute{Required: true, MarkdownDescription: "Имя бакета s3 Не изменяется после создания."}, + "cors_all": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Публичный доступ к чтению настроек позволяет любому пользователю интернета читать конфигурации CORS Не изменяется после создания."}, + "list_all": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Публичный доступ к листингу позволяет любому пользователю интернета получить список объектов в бакете. Не изменяется после создания."}, + "max_size": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("-1"), MarkdownDescription: "Максимальный размер бакета s3. Если поставить -1, будет безлимит. Размер указывается в Bytes Не изменяется после создания."}, + "placement": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("HOT"), MarkdownDescription: "Класс хранилища, который используется по умолчанию при загрузке объектов. Чем холоднее, тем дешевле место, но дороже обращения Не изменяется после создания."}, + "read_all": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Публичный доступ на чтение позволяет любому пользователю интернета скачивать объекты из бакета. Не изменяется после создания."}, + "s3_user_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UID Корневой услуги S3. На основе него будет создан бакет. Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *S3bucketResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *S3bucketModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *S3bucketModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan S3bucketModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan. + // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов. + // Resolve (displayName → UUID или uppercase → lowercase) нужен только для + // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка + // "Provider produced invalid plan: planned value does not match config value". + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.BucketName.IsNull() && !plan.BucketName.IsUnknown() && !state.BucketName.IsNull() && !state.BucketName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.BucketName.ValueString(), state.BucketName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить bucket_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.CorsAll.IsNull() && !plan.CorsAll.IsUnknown() && !state.CorsAll.IsNull() && !state.CorsAll.IsUnknown() { + if plan.CorsAll.ValueBool() != state.CorsAll.ValueBool() { + resp.Diagnostics.AddError("Нельзя изменить cors_all", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ListAll.IsNull() && !plan.ListAll.IsUnknown() && !state.ListAll.IsNull() && !state.ListAll.IsUnknown() { + if plan.ListAll.ValueBool() != state.ListAll.ValueBool() { + resp.Diagnostics.AddError("Нельзя изменить list_all", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.MaxSize.IsNull() && !plan.MaxSize.IsUnknown() && !state.MaxSize.IsNull() && !state.MaxSize.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.MaxSize.ValueString(), state.MaxSize.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить max_size", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.Placement.IsNull() && !plan.Placement.IsUnknown() && !state.Placement.IsNull() && !state.Placement.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Placement.ValueString(), state.Placement.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить placement", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ReadAll.IsNull() && !plan.ReadAll.IsUnknown() && !state.ReadAll.IsNull() && !state.ReadAll.IsUnknown() { + if plan.ReadAll.ValueBool() != state.ReadAll.ValueBool() { + resp.Diagnostics.AddError("Нельзя изменить read_all", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.S3UserUid.IsNull() && !plan.S3UserUid.IsUnknown() && !state.S3UserUid.IsNull() && !state.S3UserUid.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.S3UserUid.ValueString(), state.S3UserUid.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить s3_user_uid", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.BucketName.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "bucket_name is required.") + return + } + if config.BucketName.IsUnknown() { + return + } + if config.S3UserUid.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "s3_user_uid is required.") + return + } + if config.S3UserUid.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + if !config.S3UserUid.IsNull() && !config.S3UserUid.IsUnknown() { + resolvedS3UserUid, err := r.client.ResolveRefSvcParamValue(ctx, 12, config.S3UserUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve s3_user_uid", err.Error()) + } else if resolvedS3UserUid != "" && resolvedS3UserUid != config.S3UserUid.ValueString() { + config.S3UserUid = types.StringValue(resolvedS3UserUid) + } + } + params := map[int]string{ + 124: resources_core.FormatString(config.S3UserUid), + 125: resources_core.FormatString(config.BucketName), + 126: resources_core.FormatString(config.MaxSize), + 127: resources_core.FormatBool(config.ReadAll), + 128: resources_core.FormatBool(config.ListAll), + 129: resources_core.FormatBool(config.CorsAll), + 130: resources_core.FormatString(config.Placement), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 13, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *S3bucketResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data S3bucketModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.BucketName.IsNull() || data.BucketName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "bucket_name is required.") + return + } + if data.S3UserUid.IsNull() || data.S3UserUid.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "s3_user_uid is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 13, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 124: resources_core.FormatString(data.S3UserUid), + 125: resources_core.FormatString(data.BucketName), + 126: resources_core.FormatString(data.MaxSize), + 127: resources_core.FormatBool(data.ReadAll), + 128: resources_core.FormatBool(data.ListAll), + 129: resources_core.FormatBool(data.CorsAll), + 130: resources_core.FormatString(data.Placement), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 13, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 13, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "bucketName", Field: "BucketName", Type: "string"}, + {Code: "corsAll", Field: "CorsAll", Type: "bool"}, + {Code: "listAll", Field: "ListAll", Type: "bool"}, + {Code: "maxSize", Field: "MaxSize", Type: "string"}, + {Code: "placement", Field: "Placement", Type: "string"}, + {Code: "readAll", Field: "ReadAll", Type: "bool"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + // Restore user-provided casing в state (Create). + // EqualFold = «совпадают ли значения без учёта регистра?» + // Если да — значит API вернул «туже» строку, только в другом регистре. + // Заменяем state на original (то что было в plan/config пользователя). + // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint. + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *S3bucketResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state S3bucketModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 13, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "bucketName", Field: "BucketName", Type: "string"}, + {Code: "corsAll", Field: "CorsAll", Type: "bool"}, + {Code: "listAll", Field: "ListAll", Type: "bool"}, + {Code: "maxSize", Field: "MaxSize", Type: "string"}, + {Code: "placement", Field: "Placement", Type: "string"}, + {Code: "readAll", Field: "ReadAll", Type: "bool"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + // Restore user-provided casing в state (Read). + // При чтении у нас нет plan — но предыдущий state уже хранит значение + // в регистре пользователя (после первого Create оно было восстановлено). + // Берём prior state (переменная state) как эталон регистра. + // Если API вернул то же UUID только строчными буквами — восстанавливаем. + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *S3bucketResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan S3bucketModel + var state S3bucketModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 13, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "bucketName", Field: "BucketName", Type: "string"}, + {Code: "corsAll", Field: "CorsAll", Type: "bool"}, + {Code: "listAll", Field: "ListAll", Type: "bool"}, + {Code: "maxSize", Field: "MaxSize", Type: "string"}, + {Code: "placement", Field: "Placement", Type: "string"}, + {Code: "readAll", Field: "ReadAll", Type: "bool"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + // Restore user-provided casing в state (Update). + // После API-вызова modify state содержит значения в lower-case от API. + // Plan == config == то что написал пользователь (регистр неизменён). + // EqualFold: если UUID совпадает без учёта регистра — берём из plan. + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *S3bucketResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state S3bucketModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *S3bucketResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *S3bucketResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/148_vc_mgmt_sthutrval_cluster_resource.go b/generated/prod/go/148_vc_mgmt_sthutrval_cluster_resource.go new file mode 100644 index 0000000..92f4d9f --- /dev/null +++ b/generated/prod/go/148_vc_mgmt_sthutrval_cluster_resource.go @@ -0,0 +1,419 @@ +package resources_gen + +import ( + "context" + "fmt" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vc_mgmt_sthutrval_cluster +// Service ID: 148 + +var _ resource.Resource = &VcMgmtSthutrvalClusterResource{} +var _ resource.ResourceWithModifyPlan = &VcMgmtSthutrvalClusterResource{} +var _ resource.ResourceWithImportState = &VcMgmtSthutrvalClusterResource{} + +type VcMgmtSthutrvalClusterResource struct { + client *core.UniversalClient +} + +// VcMgmtSthutrvalClusterAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration. +type VcMgmtSthutrvalClusterAccessConfigurationModel struct { + NeedExternalAddressAPI types.Bool `tfsdk:"need_external_address_a_p_i" json:"needExternalAddressAPI"` + AccessIpListAPI types.String `tfsdk:"access_ip_list_a_p_i" json:"accessIpListAPI"` + NeedExternalAddressIngress types.Bool `tfsdk:"need_external_address_ingress" json:"needExternalAddressIngress"` + AccessIpListIngress types.String `tfsdk:"access_ip_list_ingress" json:"accessIpListIngress"` +} + +// VcMgmtSthutrvalClusterClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type VcMgmtSthutrvalClusterClusterConfigurationModel struct { + AppVersion types.String `tfsdk:"app_version" json:"appVersion"` +} + +// VcMgmtSthutrvalClusterControlPlaneConfigurationModel — вложенная модель для map-fixed параметра controlPlaneConfiguration. +type VcMgmtSthutrvalClusterControlPlaneConfigurationModel struct { + Count types.Int64 `tfsdk:"count" json:"count"` + SizingPolicy types.String `tfsdk:"sizing_policy" json:"sizingPolicy"` + SizingDisk types.Int64 `tfsdk:"sizing_disk" json:"sizingDisk"` +} + +// VcMgmtSthutrvalClusterStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type VcMgmtSthutrvalClusterStartupConfigurationModel struct { + ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"` + ClusterName types.String `tfsdk:"cluster_name" json:"clusterName"` +} + +type VcMgmtSthutrvalClusterModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessConfiguration *VcMgmtSthutrvalClusterAccessConfigurationModel `tfsdk:"access_configuration"` + ClusterConfiguration *VcMgmtSthutrvalClusterClusterConfigurationModel `tfsdk:"cluster_configuration"` + ControlPlaneConfiguration *VcMgmtSthutrvalClusterControlPlaneConfigurationModel `tfsdk:"control_plane_configuration"` + StartupConfiguration *VcMgmtSthutrvalClusterStartupConfigurationModel `tfsdk:"startup_configuration"` + WorkerConfiguration types.String `tfsdk:"worker_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewVcMgmtSthutrvalClusterResource() resource.Resource { + return &VcMgmtSthutrvalClusterResource{} +} + +func (r *VcMgmtSthutrvalClusterResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vc_mgmt_sthutrval_cluster" +} + +func (r *VcMgmtSthutrvalClusterResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "need_external_address_a_p_i": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Выделяет внешний IP для доступа к kubernetes API. Пока нельзя выставить false"}, + "access_ip_list_a_p_i": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Access IP List для подключения к Kubernetes API. Указывается массив. Если передан пустой массив, доступ выделяется всем
Пример: **["0.0.0.0/0"]** или **[]** или **["178.4.13.2", "178.4.14.0/24"]**"}, + "need_external_address_ingress": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Выделяет внешний IP для доступа к Ingress"}, + "access_ip_list_ingress": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Access IP List для подключения к Ingress. Указывается массив. Если передан пустой массив, доступ выделяется всем
Пример: **["0.0.0.0/0"]** или **[]** или **["178.4.13.2", "178.4.14.0/24"]**"}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "app_version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("2.13.1")}, + }}, + "control_plane_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "count": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)}, + "sizing_policy": schema.StringAttribute{Required: true, MarkdownDescription: "Доступные sizing Policy представлены исходя из ресурсной платформы, которая была выбрана"}, + "sizing_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(50)}, + }}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("ngcloud.ru"), MarkdownDescription: "Облако в котором будет развёрнут тенант"}, + "cluster_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("example-ewwnrebe")}, + }}, + "worker_configuration": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VcMgmtSthutrvalClusterResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *VcMgmtSthutrvalClusterModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *VcMgmtSthutrvalClusterModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan VcMgmtSthutrvalClusterModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.WorkerConfiguration.IsNull() && !plan.WorkerConfiguration.IsUnknown() && !state.WorkerConfiguration.IsNull() && !state.WorkerConfiguration.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.WorkerConfiguration.ValueString(), state.WorkerConfiguration.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить worker_configuration", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 771: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString()), "clusterName": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ClusterName.ValueString())}), + 772: resources_core.BuildJSON(map[string]string{"appVersion": fmt.Sprintf("\"%s\"", config.ClusterConfiguration.AppVersion.ValueString())}), + 773: resources_core.BuildJSON(map[string]string{"count": fmt.Sprintf("%d", config.ControlPlaneConfiguration.Count.ValueInt64()), "sizingPolicy": fmt.Sprintf("\"%s\"", config.ControlPlaneConfiguration.SizingPolicy.ValueString()), "sizingDisk": fmt.Sprintf("%d", config.ControlPlaneConfiguration.SizingDisk.ValueInt64())}), + 774: resources_core.FormatString(config.WorkerConfiguration), + 775: resources_core.BuildJSON(map[string]string{"needExternalAddressAPI": fmt.Sprintf("%v", config.AccessConfiguration.NeedExternalAddressAPI.ValueBool()), "accessIpListAPI": fmt.Sprintf("\"%s\"", config.AccessConfiguration.AccessIpListAPI.ValueString()), "needExternalAddressIngress": fmt.Sprintf("%v", config.AccessConfiguration.NeedExternalAddressIngress.ValueBool()), "accessIpListIngress": fmt.Sprintf("\"%s\"", config.AccessConfiguration.AccessIpListIngress.ValueString())}), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 148, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *VcMgmtSthutrvalClusterResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data VcMgmtSthutrvalClusterModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 148, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 771: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString()), "clusterName": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ClusterName.ValueString())}), + 772: resources_core.BuildJSON(map[string]string{"appVersion": fmt.Sprintf("\"%s\"", data.ClusterConfiguration.AppVersion.ValueString())}), + 773: resources_core.BuildJSON(map[string]string{"count": fmt.Sprintf("%d", data.ControlPlaneConfiguration.Count.ValueInt64()), "sizingPolicy": fmt.Sprintf("\"%s\"", data.ControlPlaneConfiguration.SizingPolicy.ValueString()), "sizingDisk": fmt.Sprintf("%d", data.ControlPlaneConfiguration.SizingDisk.ValueInt64())}), + 774: resources_core.FormatString(data.WorkerConfiguration), + 775: resources_core.BuildJSON(map[string]string{"needExternalAddressAPI": fmt.Sprintf("%v", data.AccessConfiguration.NeedExternalAddressAPI.ValueBool()), "accessIpListAPI": fmt.Sprintf("\"%s\"", data.AccessConfiguration.AccessIpListAPI.ValueString()), "needExternalAddressIngress": fmt.Sprintf("%v", data.AccessConfiguration.NeedExternalAddressIngress.ValueBool()), "accessIpListIngress": fmt.Sprintf("\"%s\"", data.AccessConfiguration.AccessIpListIngress.ValueString())}), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 148, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 148, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "controlPlaneConfiguration", Field: "ControlPlaneConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + {Code: "workerConfiguration", Field: "WorkerConfiguration", Type: "array-map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcMgmtSthutrvalClusterResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VcMgmtSthutrvalClusterModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 148, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "controlPlaneConfiguration", Field: "ControlPlaneConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + {Code: "workerConfiguration", Field: "WorkerConfiguration", Type: "array-map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *VcMgmtSthutrvalClusterResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VcMgmtSthutrvalClusterModel + var state VcMgmtSthutrvalClusterModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 148, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "controlPlaneConfiguration", Field: "ControlPlaneConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + {Code: "workerConfiguration", Field: "WorkerConfiguration", Type: "array-map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcMgmtSthutrvalClusterResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VcMgmtSthutrvalClusterModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *VcMgmtSthutrvalClusterResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *VcMgmtSthutrvalClusterResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/149_valo_tenant_resource.go b/generated/prod/go/149_valo_tenant_resource.go new file mode 100644 index 0000000..37670d5 --- /dev/null +++ b/generated/prod/go/149_valo_tenant_resource.go @@ -0,0 +1,431 @@ +package resources_gen + +import ( + "context" + "fmt" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: valo_tenant +// Service ID: 149 + +var _ resource.Resource = &ValoTenantResource{} +var _ resource.ResourceWithModifyPlan = &ValoTenantResource{} +var _ resource.ResourceWithImportState = &ValoTenantResource{} + +type ValoTenantResource struct { + client *core.UniversalClient +} + +// ValoTenantValoOptionsModel — вложенная модель для map-fixed параметра valoOptions. +type ValoTenantValoOptionsModel struct { + FfUserImport types.Bool `tfsdk:"ff_user_import" json:"ffUserImport"` + FfThemes types.Bool `tfsdk:"ff_themes" json:"ffThemes"` + FfPublicLink types.Bool `tfsdk:"ff_public_link" json:"ffPublicLink"` + FfLogging types.Bool `tfsdk:"ff_logging" json:"ffLogging"` + UserCount types.Int64 `tfsdk:"user_count" json:"userCount"` + RequestApproveMaxPeriod types.Int64 `tfsdk:"request_approve_max_period" json:"requestApproveMaxPeriod"` + FfOnlineEditor types.Bool `tfsdk:"ff_online_editor" json:"ffOnlineEditor"` + FfWatermark types.Bool `tfsdk:"ff_watermark" json:"ffWatermark"` +} + +type ValoTenantModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + Domain types.String `tfsdk:"domain"` + ResourceRealm types.String `tfsdk:"resource_realm"` + S3Storage types.Int64 `tfsdk:"s3_storage"` + ValoOptions *ValoTenantValoOptionsModel `tfsdk:"valo_options"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewValoTenantResource() resource.Resource { + return &ValoTenantResource{} +} + +func (r *ValoTenantResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_valo_tenant" +} + +func (r *ValoTenantResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "domain": schema.StringAttribute{Required: true, MarkdownDescription: "Будет добавлено в зону valo.ngcloud.ru Не изменяется после создания."}, + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("valo.ngcloud.ru"), MarkdownDescription: "Не изменяется после создания."}, + "s3_storage": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Параметр указывается в `Gb`"}, + "valo_options": schema.SingleNestedAttribute{Optional: true, Computed: true, + Attributes: map[string]schema.Attribute{ + "ff_user_import": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Тарифная опция"}, + "ff_themes": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Тарифная опция"}, + "ff_public_link": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Тарифная опция"}, + "ff_logging": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Тарифная опция"}, + "user_count": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10)}, + "request_approve_max_period": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2), MarkdownDescription: "Максимальное значение периода согласования (в днях)"}, + "ff_online_editor": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Тарифная опция"}, + "ff_watermark": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Тарифная опция"}, + }}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *ValoTenantResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *ValoTenantModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *ValoTenantModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan ValoTenantModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.Domain.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "domain is required.") + return + } + if config.Domain.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 655: resources_core.FormatString(config.ResourceRealm), + 656: resources_core.FormatString(config.Domain), + 657: resources_core.BuildJSON(map[string]string{"ffUserImport": fmt.Sprintf("%v", config.ValoOptions.FfUserImport.ValueBool()), "ffThemes": fmt.Sprintf("%v", config.ValoOptions.FfThemes.ValueBool()), "ffPublicLink": fmt.Sprintf("%v", config.ValoOptions.FfPublicLink.ValueBool()), "ffLogging": fmt.Sprintf("%v", config.ValoOptions.FfLogging.ValueBool()), "userCount": fmt.Sprintf("%d", config.ValoOptions.UserCount.ValueInt64()), "requestApproveMaxPeriod": fmt.Sprintf("%d", config.ValoOptions.RequestApproveMaxPeriod.ValueInt64()), "ffOnlineEditor": fmt.Sprintf("%v", config.ValoOptions.FfOnlineEditor.ValueBool()), "ffWatermark": fmt.Sprintf("%v", config.ValoOptions.FfWatermark.ValueBool())}), + 658: resources_core.FormatInt64(config.S3Storage), + } + desiredDomain := "" + if !config.Domain.IsNull() && !config.Domain.IsUnknown() { + desiredDomain = config.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 149, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *ValoTenantResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data ValoTenantModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.Domain.IsNull() || data.Domain.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "domain is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + if !data.Domain.IsNull() && !data.Domain.IsUnknown() { + desiredDomain = data.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 149, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 655: resources_core.FormatString(data.ResourceRealm), + 656: resources_core.FormatString(data.Domain), + 657: resources_core.BuildJSON(map[string]string{"ffUserImport": fmt.Sprintf("%v", data.ValoOptions.FfUserImport.ValueBool()), "ffThemes": fmt.Sprintf("%v", data.ValoOptions.FfThemes.ValueBool()), "ffPublicLink": fmt.Sprintf("%v", data.ValoOptions.FfPublicLink.ValueBool()), "ffLogging": fmt.Sprintf("%v", data.ValoOptions.FfLogging.ValueBool()), "userCount": fmt.Sprintf("%d", data.ValoOptions.UserCount.ValueInt64()), "requestApproveMaxPeriod": fmt.Sprintf("%d", data.ValoOptions.RequestApproveMaxPeriod.ValueInt64()), "ffOnlineEditor": fmt.Sprintf("%v", data.ValoOptions.FfOnlineEditor.ValueBool()), "ffWatermark": fmt.Sprintf("%v", data.ValoOptions.FfWatermark.ValueBool())}), + 658: resources_core.FormatInt64(data.S3Storage), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 149, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 149, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "s3Storage", Field: "S3Storage", Type: "int64"}, + {Code: "valoOptions", Field: "ValoOptions", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *ValoTenantResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state ValoTenantModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 149, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "s3Storage", Field: "S3Storage", Type: "int64"}, + {Code: "valoOptions", Field: "ValoOptions", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *ValoTenantResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan ValoTenantModel + var state ValoTenantModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.S3Storage.IsNull() != state.S3Storage.IsNull() || plan.S3Storage.IsUnknown() != state.S3Storage.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.S3Storage.IsNull() && !plan.S3Storage.IsUnknown() && !state.S3Storage.IsNull() && !state.S3Storage.IsUnknown() { + if plan.S3Storage.ValueInt64() != state.S3Storage.ValueInt64() { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 659: resources_core.BuildJSON(map[string]string{"ffPublicLink": fmt.Sprintf("%v", plan.ValoOptions.FfPublicLink.ValueBool()), "ffLogging": fmt.Sprintf("%v", plan.ValoOptions.FfLogging.ValueBool()), "userCount": fmt.Sprintf("%d", plan.ValoOptions.UserCount.ValueInt64()), "requestApproveMaxPeriod": fmt.Sprintf("%d", plan.ValoOptions.RequestApproveMaxPeriod.ValueInt64()), "ffOnlineEditor": fmt.Sprintf("%v", plan.ValoOptions.FfOnlineEditor.ValueBool()), "ffWatermark": fmt.Sprintf("%v", plan.ValoOptions.FfWatermark.ValueBool()), "ffUserImport": fmt.Sprintf("%v", plan.ValoOptions.FfUserImport.ValueBool()), "ffThemes": fmt.Sprintf("%v", plan.ValoOptions.FfThemes.ValueBool())}), + 660: resources_core.FormatInt64(plan.S3Storage), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 149, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "s3Storage", Field: "S3Storage", Type: "int64"}, + {Code: "valoOptions", Field: "ValoOptions", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *ValoTenantResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state ValoTenantModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *ValoTenantResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *ValoTenantResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/150_k8s_sthutrval_cluster_resource.go b/generated/prod/go/150_k8s_sthutrval_cluster_resource.go new file mode 100644 index 0000000..34f08a9 --- /dev/null +++ b/generated/prod/go/150_k8s_sthutrval_cluster_resource.go @@ -0,0 +1,429 @@ +package resources_gen + +import ( + "context" + "fmt" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: k8s_sthutrval_cluster +// Service ID: 150 + +var _ resource.Resource = &K8sSthutrvalClusterResource{} +var _ resource.ResourceWithModifyPlan = &K8sSthutrvalClusterResource{} +var _ resource.ResourceWithImportState = &K8sSthutrvalClusterResource{} + +type K8sSthutrvalClusterResource struct { + client *core.UniversalClient +} + +// K8sSthutrvalClusterAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration. +type K8sSthutrvalClusterAccessConfigurationModel struct { + NeedExternalAddressAPI types.Bool `tfsdk:"need_external_address_a_p_i" json:"needExternalAddressAPI"` + AccessIpListAPI types.String `tfsdk:"access_ip_list_a_p_i" json:"accessIpListAPI"` + NeedExternalAddressIngress types.Bool `tfsdk:"need_external_address_ingress" json:"needExternalAddressIngress"` + AccessIpListIngress types.String `tfsdk:"access_ip_list_ingress" json:"accessIpListIngress"` +} + +// K8sSthutrvalClusterClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type K8sSthutrvalClusterClusterConfigurationModel struct { + AppVersion types.String `tfsdk:"app_version" json:"appVersion"` +} + +// K8sSthutrvalClusterControlPlaneConfigurationModel — вложенная модель для map-fixed параметра controlPlaneConfiguration. +type K8sSthutrvalClusterControlPlaneConfigurationModel struct { + SizingPolicy types.String `tfsdk:"sizing_policy" json:"sizingPolicy"` + SizingDisk types.Int64 `tfsdk:"sizing_disk" json:"sizingDisk"` + Count types.Int64 `tfsdk:"count" json:"count"` +} + +// K8sSthutrvalClusterStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type K8sSthutrvalClusterStartupConfigurationModel struct { + VdcUid types.String `tfsdk:"vdc_uid" json:"vdcUid"` + NsxtUid types.String `tfsdk:"nsxt_uid" json:"nsxtUid"` + ClusterName types.String `tfsdk:"cluster_name" json:"clusterName"` +} + +type K8sSthutrvalClusterModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessConfiguration *K8sSthutrvalClusterAccessConfigurationModel `tfsdk:"access_configuration"` + ClusterConfiguration *K8sSthutrvalClusterClusterConfigurationModel `tfsdk:"cluster_configuration"` + ControlPlaneConfiguration *K8sSthutrvalClusterControlPlaneConfigurationModel `tfsdk:"control_plane_configuration"` + StartupConfiguration *K8sSthutrvalClusterStartupConfigurationModel `tfsdk:"startup_configuration"` + WorkerConfiguration types.String `tfsdk:"worker_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewK8sSthutrvalClusterResource() resource.Resource { + return &K8sSthutrvalClusterResource{} +} + +func (r *K8sSthutrvalClusterResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_k8s_sthutrval_cluster" +} + +func (r *K8sSthutrvalClusterResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "need_external_address_a_p_i": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Выделяет внешний IP для доступа к kubernetes API. Пока нельзя выставить false"}, + "access_ip_list_a_p_i": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Access IP List для подключения к Kubernetes API. Указывается массив. Если передан пустой массив, доступ выделяется всем"}, + "need_external_address_ingress": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Выделяет внешний IP для доступа к Ingress"}, + "access_ip_list_ingress": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Access IP List для подключения к Ingress. Указывается массив. Если передан пустой массив, доступ выделяется всем"}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "app_version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("2.13.1")}, + }}, + "control_plane_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "sizing_policy": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Доступные sizing Policy можно обновлять через услугу `Виртуальный датацентр`"}, + "sizing_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(50), MarkdownDescription: "Дисковая политика берётся из услуги VDC, первый в списке"}, + "count": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)}, + }}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "vdc_uid": schema.StringAttribute{Required: true}, + "nsxt_uid": schema.StringAttribute{Required: true}, + "cluster_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("example-wln2kdvp")}, + }}, + "worker_configuration": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]")}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *K8sSthutrvalClusterResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *K8sSthutrvalClusterModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *K8sSthutrvalClusterModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan K8sSthutrvalClusterModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 750: resources_core.BuildJSON(map[string]string{"vdcUid": fmt.Sprintf("\"%s\"", config.StartupConfiguration.VdcUid.ValueString()), "nsxtUid": fmt.Sprintf("\"%s\"", config.StartupConfiguration.NsxtUid.ValueString()), "clusterName": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ClusterName.ValueString())}), + 751: resources_core.BuildJSON(map[string]string{"sizingPolicy": fmt.Sprintf("\"%s\"", config.ControlPlaneConfiguration.SizingPolicy.ValueString()), "sizingDisk": fmt.Sprintf("%d", config.ControlPlaneConfiguration.SizingDisk.ValueInt64()), "count": fmt.Sprintf("%d", config.ControlPlaneConfiguration.Count.ValueInt64())}), + 752: resources_core.FormatString(config.WorkerConfiguration), + 753: resources_core.BuildJSON(map[string]string{"needExternalAddressAPI": fmt.Sprintf("%v", config.AccessConfiguration.NeedExternalAddressAPI.ValueBool()), "accessIpListAPI": fmt.Sprintf("\"%s\"", config.AccessConfiguration.AccessIpListAPI.ValueString()), "needExternalAddressIngress": fmt.Sprintf("%v", config.AccessConfiguration.NeedExternalAddressIngress.ValueBool()), "accessIpListIngress": fmt.Sprintf("\"%s\"", config.AccessConfiguration.AccessIpListIngress.ValueString())}), + 755: resources_core.BuildJSON(map[string]string{"appVersion": fmt.Sprintf("\"%s\"", config.ClusterConfiguration.AppVersion.ValueString())}), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 150, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *K8sSthutrvalClusterResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data K8sSthutrvalClusterModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 150, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 750: resources_core.BuildJSON(map[string]string{"vdcUid": fmt.Sprintf("\"%s\"", data.StartupConfiguration.VdcUid.ValueString()), "nsxtUid": fmt.Sprintf("\"%s\"", data.StartupConfiguration.NsxtUid.ValueString()), "clusterName": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ClusterName.ValueString())}), + 751: resources_core.BuildJSON(map[string]string{"sizingPolicy": fmt.Sprintf("\"%s\"", data.ControlPlaneConfiguration.SizingPolicy.ValueString()), "sizingDisk": fmt.Sprintf("%d", data.ControlPlaneConfiguration.SizingDisk.ValueInt64()), "count": fmt.Sprintf("%d", data.ControlPlaneConfiguration.Count.ValueInt64())}), + 752: resources_core.FormatString(data.WorkerConfiguration), + 753: resources_core.BuildJSON(map[string]string{"needExternalAddressAPI": fmt.Sprintf("%v", data.AccessConfiguration.NeedExternalAddressAPI.ValueBool()), "accessIpListAPI": fmt.Sprintf("\"%s\"", data.AccessConfiguration.AccessIpListAPI.ValueString()), "needExternalAddressIngress": fmt.Sprintf("%v", data.AccessConfiguration.NeedExternalAddressIngress.ValueBool()), "accessIpListIngress": fmt.Sprintf("\"%s\"", data.AccessConfiguration.AccessIpListIngress.ValueString())}), + 755: resources_core.BuildJSON(map[string]string{"appVersion": fmt.Sprintf("\"%s\"", data.ClusterConfiguration.AppVersion.ValueString())}), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 150, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 150, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "controlPlaneConfiguration", Field: "ControlPlaneConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + {Code: "workerConfiguration", Field: "WorkerConfiguration", Type: "array-map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *K8sSthutrvalClusterResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state K8sSthutrvalClusterModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 150, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "controlPlaneConfiguration", Field: "ControlPlaneConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + {Code: "workerConfiguration", Field: "WorkerConfiguration", Type: "array-map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *K8sSthutrvalClusterResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan K8sSthutrvalClusterModel + var state K8sSthutrvalClusterModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.WorkerConfiguration.IsNull() != state.WorkerConfiguration.IsNull() || plan.WorkerConfiguration.IsUnknown() != state.WorkerConfiguration.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.WorkerConfiguration.IsNull() && !plan.WorkerConfiguration.IsUnknown() && !state.WorkerConfiguration.IsNull() && !state.WorkerConfiguration.IsUnknown() { + if !strings.EqualFold(plan.WorkerConfiguration.ValueString(), state.WorkerConfiguration.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 756: resources_core.BuildJSON(map[string]string{"appVersion": fmt.Sprintf("\"%s\"", plan.ClusterConfiguration.AppVersion.ValueString())}), + 757: resources_core.BuildJSON(map[string]string{"sizingDisk": fmt.Sprintf("%d", plan.ControlPlaneConfiguration.SizingDisk.ValueInt64()), "count": fmt.Sprintf("%d", plan.ControlPlaneConfiguration.Count.ValueInt64()), "sizingPolicy": fmt.Sprintf("\"%s\"", plan.ControlPlaneConfiguration.SizingPolicy.ValueString())}), + 758: resources_core.FormatString(plan.WorkerConfiguration), + 759: resources_core.BuildJSON(map[string]string{"needExternalAddressAPI": fmt.Sprintf("%v", plan.AccessConfiguration.NeedExternalAddressAPI.ValueBool()), "accessIpListAPI": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.AccessIpListAPI.ValueString()), "needExternalAddressIngress": fmt.Sprintf("%v", plan.AccessConfiguration.NeedExternalAddressIngress.ValueBool()), "accessIpListIngress": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.AccessIpListIngress.ValueString())}), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 150, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "controlPlaneConfiguration", Field: "ControlPlaneConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + {Code: "workerConfiguration", Field: "WorkerConfiguration", Type: "array-map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *K8sSthutrvalClusterResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state K8sSthutrvalClusterModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *K8sSthutrvalClusterResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *K8sSthutrvalClusterResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/150_k8s_sthutrval_cluster_user_resource.go b/generated/prod/go/150_k8s_sthutrval_cluster_user_resource.go new file mode 100644 index 0000000..55faf0e --- /dev/null +++ b/generated/prod/go/150_k8s_sthutrval_cluster_user_resource.go @@ -0,0 +1,343 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: k8s_sthutrval_cluster +// Subresource: user + +var _ resource.Resource = &K8sSthutrvalClusterUserResource{} + +// K8sSthutrvalClusterUserResource manages a subresource via operations. +type K8sSthutrvalClusterUserResource struct { + client *core.UniversalClient +} + +type K8sSthutrvalClusterUserModel struct { + ID types.String `tfsdk:"id"` + K8sSthutrvalClusterID types.String `tfsdk:"k8s_sthutrval_cluster_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + Admins types.String `tfsdk:"admins"` +} + +func NewK8sSthutrvalClusterUserResource() resource.Resource { + return &K8sSthutrvalClusterUserResource{} +} + +func (r *K8sSthutrvalClusterUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_k8s_sthutrval_cluster_user" +} + +func (r *K8sSthutrvalClusterUserResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "k8s_sthutrval_cluster_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "admins": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *K8sSthutrvalClusterUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan K8sSthutrvalClusterUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.K8sSthutrvalClusterID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "admins": resources_core.FormatString(plan.Admins), + } + identityCodes := []string{ + "admins", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "admins": resources_core.FormatString(plan.Admins), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *K8sSthutrvalClusterUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state K8sSthutrvalClusterUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *K8sSthutrvalClusterUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan K8sSthutrvalClusterUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state K8sSthutrvalClusterUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.K8sSthutrvalClusterID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{ + "admins": resources_core.FormatString(plan.Admins), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *K8sSthutrvalClusterUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state K8sSthutrvalClusterUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_user" == "" { + return + } + + instanceUID := strings.TrimSpace(state.K8sSthutrvalClusterID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "admins": resources_core.FormatString(state.Admins), + } + identityCodes := []string{ + "admins", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "admins": resources_core.FormatString(state.Admins), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_user", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *K8sSthutrvalClusterUserResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/163_llm_ai_resource.go b/generated/prod/go/163_llm_ai_resource.go new file mode 100644 index 0000000..506e337 --- /dev/null +++ b/generated/prod/go/163_llm_ai_resource.go @@ -0,0 +1,345 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: llm_ai +// Service ID: 163 + +var _ resource.Resource = &LlmAiResource{} +var _ resource.ResourceWithModifyPlan = &LlmAiResource{} +var _ resource.ResourceWithImportState = &LlmAiResource{} + +type LlmAiResource struct { + client *core.UniversalClient +} + +type LlmAiModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + ResourceRealm types.String `tfsdk:"resource_realm"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewLlmAiResource() resource.Resource { + return &LlmAiResource{} +} + +func (r *LlmAiResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_llm_ai" +} + +func (r *LlmAiResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("llmAi.aillm.ru"), MarkdownDescription: "Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *LlmAiResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *LlmAiModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *LlmAiModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan LlmAiModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 826: resources_core.FormatString(config.ResourceRealm), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 163, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *LlmAiResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data LlmAiModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 163, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 826: resources_core.FormatString(data.ResourceRealm), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 163, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 163, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *LlmAiResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state LlmAiModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 163, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *LlmAiResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan LlmAiModel + var state LlmAiModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 163, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *LlmAiResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state LlmAiModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *LlmAiResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *LlmAiResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/163_llm_ai_secrets_resource.go b/generated/prod/go/163_llm_ai_secrets_resource.go new file mode 100644 index 0000000..063f1fb --- /dev/null +++ b/generated/prod/go/163_llm_ai_secrets_resource.go @@ -0,0 +1,389 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: llm_ai +// Subresource: secrets + +var _ resource.Resource = &LlmAiSecretsResource{} + +// LlmAiSecretsResource manages a subresource via operations. +type LlmAiSecretsResource struct { + client *core.UniversalClient +} + +type LlmAiSecretsModel struct { + ID types.String `tfsdk:"id"` + LlmAiID types.String `tfsdk:"llm_ai_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + KeyName types.String `tfsdk:"key_name"` + Models types.String `tfsdk:"models"` + RpmLimit types.Int64 `tfsdk:"rpm_limit"` + TeamName types.String `tfsdk:"team_name"` + TpmLimit types.Int64 `tfsdk:"tpm_limit"` +} + +func NewLlmAiSecretsResource() resource.Resource { + return &LlmAiSecretsResource{} +} + +func (r *LlmAiSecretsResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_llm_ai_secrets" +} + +func (r *LlmAiSecretsResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "llm_ai_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "key_name": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "models": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "rpm_limit": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(60), MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.Int64{int64planmodifier.RequiresReplace()}}, + "team_name": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "tpm_limit": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100000), MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.Int64{int64planmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *LlmAiSecretsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan LlmAiSecretsModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if plan.KeyName.IsNull() || plan.KeyName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "key_name is required.") + return + } + if plan.Models.IsNull() || plan.Models.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "models is required.") + return + } + if plan.TeamName.IsNull() || plan.TeamName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "team_name is required.") + return + } + + instanceUID := strings.TrimSpace(plan.LlmAiID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "keyName": resources_core.FormatString(plan.KeyName), + "models": resources_core.FormatString(plan.Models), + "rpmLimit": resources_core.FormatInt64(plan.RpmLimit), + "teamName": resources_core.FormatString(plan.TeamName), + "tpmLimit": resources_core.FormatInt64(plan.TpmLimit), + } + identityCodes := []string{ + "keyName", + "models", + "rpmLimit", + "teamName", + "tpmLimit", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("secrets", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "secrets", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "models": resources_core.FormatString(plan.Models), + "rpmLimit": resources_core.FormatInt64(plan.RpmLimit), + "tpmLimit": resources_core.FormatInt64(plan.TpmLimit), + "keyName": resources_core.FormatString(plan.KeyName), + "teamName": resources_core.FormatString(plan.TeamName), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_secrets", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_secrets", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "secrets", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "secrets", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "secrets", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *LlmAiSecretsResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state LlmAiSecretsModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *LlmAiSecretsResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan LlmAiSecretsModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state LlmAiSecretsModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.LlmAiID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{ + "keyName": resources_core.FormatString(plan.KeyName), + "models": resources_core.FormatString(plan.Models), + "rpmLimit": resources_core.FormatInt64(plan.RpmLimit), + "teamName": resources_core.FormatString(plan.TeamName), + "tpmLimit": resources_core.FormatInt64(plan.TpmLimit), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "secrets", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *LlmAiSecretsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state LlmAiSecretsModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_secrets" == "" { + return + } + + instanceUID := strings.TrimSpace(state.LlmAiID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "keyName": resources_core.FormatString(state.KeyName), + "models": resources_core.FormatString(state.Models), + "rpmLimit": resources_core.FormatInt64(state.RpmLimit), + "teamName": resources_core.FormatString(state.TeamName), + "tpmLimit": resources_core.FormatInt64(state.TpmLimit), + } + identityCodes := []string{ + "keyName", + "models", + "rpmLimit", + "teamName", + "tpmLimit", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("secrets", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "keyName": resources_core.FormatString(state.KeyName), + "teamName": resources_core.FormatString(state.TeamName), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_secrets", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *LlmAiSecretsResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/163_llm_ai_team_resource.go b/generated/prod/go/163_llm_ai_team_resource.go new file mode 100644 index 0000000..e411362 --- /dev/null +++ b/generated/prod/go/163_llm_ai_team_resource.go @@ -0,0 +1,365 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: llm_ai +// Subresource: team + +var _ resource.Resource = &LlmAiTeamResource{} + +// LlmAiTeamResource manages a subresource via operations. +type LlmAiTeamResource struct { + client *core.UniversalClient +} + +type LlmAiTeamModel struct { + ID types.String `tfsdk:"id"` + LlmAiID types.String `tfsdk:"llm_ai_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + Models types.String `tfsdk:"models"` + RpmLimit types.Int64 `tfsdk:"rpm_limit"` + TeamName types.String `tfsdk:"team_name"` + TpmLimit types.Int64 `tfsdk:"tpm_limit"` +} + +func NewLlmAiTeamResource() resource.Resource { + return &LlmAiTeamResource{} +} + +func (r *LlmAiTeamResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_llm_ai_team" +} + +func (r *LlmAiTeamResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "llm_ai_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "models": schema.StringAttribute{Required: true}, + "rpm_limit": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(60)}, + "team_name": schema.StringAttribute{Required: true, MarkdownDescription: "Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "tpm_limit": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100000)}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *LlmAiTeamResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan LlmAiTeamModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if plan.Models.IsNull() || plan.Models.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "models is required.") + return + } + if plan.TeamName.IsNull() || plan.TeamName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "team_name is required.") + return + } + + instanceUID := strings.TrimSpace(plan.LlmAiID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "teamName": resources_core.FormatString(plan.TeamName), + } + identityCodes := []string{ + "teamName", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("team", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "team", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "teamName": resources_core.FormatString(plan.TeamName), + "models": resources_core.FormatString(plan.Models), + "rpmLimit": resources_core.FormatInt64(plan.RpmLimit), + "tpmLimit": resources_core.FormatInt64(plan.TpmLimit), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_team", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_team", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "team", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "team", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "team", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *LlmAiTeamResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state LlmAiTeamModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *LlmAiTeamResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan LlmAiTeamModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "modify_team" == "" { + var state LlmAiTeamModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.LlmAiID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{ + "teamName": resources_core.FormatString(plan.TeamName), + "models": resources_core.FormatString(plan.Models), + "rpmLimit": resources_core.FormatInt64(plan.RpmLimit), + "tpmLimit": resources_core.FormatInt64(plan.TpmLimit), + }) + + idParams := map[string]string{ + "teamName": resources_core.FormatString(plan.TeamName), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "modify_team", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "team", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *LlmAiTeamResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state LlmAiTeamModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_team" == "" { + return + } + + instanceUID := strings.TrimSpace(state.LlmAiID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "teamName": resources_core.FormatString(state.TeamName), + } + identityCodes := []string{ + "teamName", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("team", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "teamName": resources_core.FormatString(state.TeamName), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_team", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *LlmAiTeamResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/19_vc_org_resource.go b/generated/prod/go/19_vc_org_resource.go new file mode 100644 index 0000000..b1e8dc9 --- /dev/null +++ b/generated/prod/go/19_vc_org_resource.go @@ -0,0 +1,391 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vc_org +// Service ID: 19 + +var _ resource.Resource = &VcOrgResource{} +var _ resource.ResourceWithModifyPlan = &VcOrgResource{} +var _ resource.ResourceWithImportState = &VcOrgResource{} + +type VcOrgResource struct { + client *core.UniversalClient +} + +type VcOrgModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + OrganizationType types.String `tfsdk:"organization_type"` + ResourceRealm types.String `tfsdk:"resource_realm"` + VIPConfigure types.String `tfsdk:"v_i_p_configure"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewVcOrgResource() resource.Resource { + return &VcOrgResource{} +} + +func (r *VcOrgResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vc_org" +} + +func (r *VcOrgResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "organization_type": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("iaas"), MarkdownDescription: "Доступ к организации предоставляется только при выборе `iaas` Не изменяется после создания."}, + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("ngcloud.ru"), MarkdownDescription: "Облако в котором будет развёрнут тенант Не изменяется после создания."}, + "v_i_p_configure": schema.StringAttribute{Required: true}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VcOrgResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *VcOrgModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *VcOrgModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan VcOrgModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.OrganizationType.IsNull() && !plan.OrganizationType.IsUnknown() && !state.OrganizationType.IsNull() && !state.OrganizationType.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.OrganizationType.ValueString(), state.OrganizationType.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить organization_type", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.VIPConfigure.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "v_i_p_configure is required.") + return + } + if config.VIPConfigure.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 418: resources_core.FormatString(config.ResourceRealm), + 556: resources_core.FormatString(config.OrganizationType), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 19, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *VcOrgResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data VcOrgModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.VIPConfigure.IsNull() || data.VIPConfigure.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "v_i_p_configure is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 19, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 418: resources_core.FormatString(data.ResourceRealm), + 556: resources_core.FormatString(data.OrganizationType), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 19, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 19, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "organizationType", Field: "OrganizationType", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "vIPConfigure", Field: "VIPConfigure", Type: "array-map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcOrgResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VcOrgModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 19, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "organizationType", Field: "OrganizationType", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "vIPConfigure", Field: "VIPConfigure", Type: "array-map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *VcOrgResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VcOrgModel + var state VcOrgModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.VIPConfigure.IsNull() != state.VIPConfigure.IsNull() || plan.VIPConfigure.IsUnknown() != state.VIPConfigure.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.VIPConfigure.IsNull() && !plan.VIPConfigure.IsUnknown() && !state.VIPConfigure.IsNull() && !state.VIPConfigure.IsUnknown() { + if !strings.EqualFold(plan.VIPConfigure.ValueString(), state.VIPConfigure.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 662: resources_core.FormatString(plan.VIPConfigure), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 19, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "organizationType", Field: "OrganizationType", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "vIPConfigure", Field: "VIPConfigure", Type: "array-map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcOrgResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VcOrgModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *VcOrgResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *VcOrgResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/19_vc_org_user_resource.go b/generated/prod/go/19_vc_org_user_resource.go new file mode 100644 index 0000000..b18fcdd --- /dev/null +++ b/generated/prod/go/19_vc_org_user_resource.go @@ -0,0 +1,343 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vc_org +// Subresource: user + +var _ resource.Resource = &VcOrgUserResource{} + +// VcOrgUserResource manages a subresource via operations. +type VcOrgUserResource struct { + client *core.UniversalClient +} + +type VcOrgUserModel struct { + ID types.String `tfsdk:"id"` + VcOrgID types.String `tfsdk:"vc_org_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + Admins types.String `tfsdk:"admins"` +} + +func NewVcOrgUserResource() resource.Resource { + return &VcOrgUserResource{} +} + +func (r *VcOrgUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vc_org_user" +} + +func (r *VcOrgUserResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "vc_org_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "admins": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VcOrgUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan VcOrgUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.VcOrgID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "admins": resources_core.FormatString(plan.Admins), + } + identityCodes := []string{ + "admins", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "admins": resources_core.FormatString(plan.Admins), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *VcOrgUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VcOrgUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcOrgUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VcOrgUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state VcOrgUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.VcOrgID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{ + "admins": resources_core.FormatString(plan.Admins), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *VcOrgUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VcOrgUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_user" == "" { + return + } + + instanceUID := strings.TrimSpace(state.VcOrgID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "admins": resources_core.FormatString(state.Admins), + } + identityCodes := []string{ + "admins", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "admins": resources_core.FormatString(state.Admins), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_user", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *VcOrgUserResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/1_dummy_resource.go b/generated/prod/go/1_dummy_resource.go new file mode 100644 index 0000000..9867e09 --- /dev/null +++ b/generated/prod/go/1_dummy_resource.go @@ -0,0 +1,593 @@ +package resources_gen + +import ( + "context" + "fmt" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: dummy +// Service ID: 1 + +var _ resource.Resource = &DummyResource{} +var _ resource.ResourceWithModifyPlan = &DummyResource{} +var _ resource.ResourceWithImportState = &DummyResource{} + +type DummyResource struct { + client *core.UniversalClient +} + +// DummyMapExampleModel — вложенная модель для map-fixed параметра mapExample. +type DummyMapExampleModel struct { + Subparam1 types.String `tfsdk:"subparam1" json:"subparam1"` + Secret types.String `tfsdk:"secret" json:"secret"` + Subparam2 types.String `tfsdk:"subparam2" json:"subparam2"` +} + +// DummyMapFixedModel — вложенная модель для map-fixed параметра mapFixed. +type DummyMapFixedModel struct { + Bol1 types.Bool `tfsdk:"bol1" json:"bol1"` + MinStr1 types.String `tfsdk:"min_str1" json:"minStr1"` + Param1 types.Int64 `tfsdk:"param1" json:"param1"` + Param2 types.String `tfsdk:"param2" json:"param2"` + Param3 types.Int64 `tfsdk:"param3" json:"param3"` +} + +type DummyModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + // --- Redeploy support (ARCHITECTURE.md) --- + // git_revision: при изменении вызывает redeploy вместо modify. + // Опциональное поле — если не задано, modify работает как обычно. + GitRevision types.String `tfsdk:"git_revision"` + Arr types.String `tfsdk:"arr"` + ArrayMapFixedExample types.String `tfsdk:"array_map_fixed_example"` + Bodymessage types.String `tfsdk:"bodymessage"` + DurationMs types.Int64 `tfsdk:"duration_ms"` + FailAtStart types.Bool `tfsdk:"fail_at_start"` + FailInProgress types.Bool `tfsdk:"fail_in_progress"` + JsonExample types.String `tfsdk:"json_example"` + MapExample *DummyMapExampleModel `tfsdk:"map_example"` + MapFixed *DummyMapFixedModel `tfsdk:"map_fixed"` + NestedRefExample types.String `tfsdk:"nested_ref_example"` + ResourceRealm types.String `tfsdk:"resource_realm"` + WhereFail types.Int64 `tfsdk:"where_fail"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewDummyResource() resource.Resource { + return &DummyResource{} +} + +func (r *DummyResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_dummy" +} + +func (r *DummyResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "arr": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания."}, + "array_map_fixed_example": schema.StringAttribute{Optional: true, MarkdownDescription: "Array of map-fixed Не изменяется после создания."}, + "bodymessage": schema.StringAttribute{Optional: true, MarkdownDescription: "Дата, уходящая в Vault"}, + "duration_ms": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(0), MarkdownDescription: "перед завершением подождать (sleep) столько миллисекунд"}, + "fail_at_start": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "если true, завершить джоб, ничего не записывая в cmdb"}, + "fail_in_progress": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "если true, корректно начать выполнение джоба, но в процессе завершить аварийно, ничего не записывая в cmdb"}, + "json_example": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "Для тестирования интерфейса", PlanModifiers: []planmodifier.String{resources_core.JsonNormalize()}}, + "map_example": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "subparam1": schema.StringAttribute{Optional: true, MarkdownDescription: "это для администратора"}, + "secret": schema.StringAttribute{Optional: true, MarkdownDescription: "это секрет"}, + "subparam2": schema.StringAttribute{Optional: true}, + }}, + "map_fixed": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "bol1": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Булевский, обязательный"}, + "min_str1": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("abracadabra"), MarkdownDescription: "не меньше 10 символов"}, + "param1": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Integer, secret"}, + "param2": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("1"), MarkdownDescription: "Дескрипшен"}, + "param3": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "целое положительное"}, + }}, + "nested_ref_example": schema.StringAttribute{Optional: true, MarkdownDescription: "Не изменяется после создания."}, + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + "where_fail": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "принимает 1,2,3. Работает с failInProgress. 1 - упасть в prepare. 2 - упасть внутри блока Data fill. 3 - упасть после vault write"}, + // --- Redeploy support (ARCHITECTURE.md) --- + // При изменении git_revision вызывается redeploy вместо modify. + // Если не задано — modify работает без изменений. + "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *DummyResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *DummyModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *DummyModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan DummyModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.Arr.IsNull() && !plan.Arr.IsUnknown() && !state.Arr.IsNull() && !state.Arr.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Arr.ValueString(), state.Arr.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить arr", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ArrayMapFixedExample.IsNull() && !plan.ArrayMapFixedExample.IsUnknown() && !state.ArrayMapFixedExample.IsNull() && !state.ArrayMapFixedExample.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ArrayMapFixedExample.ValueString(), state.ArrayMapFixedExample.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить array_map_fixed_example", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.NestedRefExample.IsNull() && !plan.NestedRefExample.IsUnknown() && !state.NestedRefExample.IsNull() && !state.NestedRefExample.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.NestedRefExample.ValueString(), state.NestedRefExample.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить nested_ref_example", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.Arr.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "arr is required.") + return + } + if config.Arr.IsUnknown() { + return + } + if config.ResourceRealm.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + return + } + if config.ResourceRealm.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 198: resources_core.FormatInt64(config.DurationMs), + 199: resources_core.FormatBool(config.FailAtStart), + 200: resources_core.FormatBool(config.FailInProgress), + 201: resources_core.FormatInt64(config.WhereFail), + 242: resources_core.FormatString(config.ResourceRealm), + 286: resources_core.FormatString(config.Bodymessage), + 321: resources_core.BuildJSON(map[string]string{"subparam1": fmt.Sprintf("\"%s\"", config.MapExample.Subparam1.ValueString()), "secret": fmt.Sprintf("\"%s\"", config.MapExample.Secret.ValueString()), "subparam2": fmt.Sprintf("\"%s\"", config.MapExample.Subparam2.ValueString())}), + 322: resources_core.FormatString(config.JsonExample), + 396: resources_core.FormatString(config.NestedRefExample), + 647: resources_core.BuildJSON(map[string]string{"bol1": fmt.Sprintf("%v", config.MapFixed.Bol1.ValueBool()), "minStr1": fmt.Sprintf("\"%s\"", config.MapFixed.MinStr1.ValueString()), "param1": fmt.Sprintf("%d", config.MapFixed.Param1.ValueInt64()), "param2": fmt.Sprintf("\"%s\"", config.MapFixed.Param2.ValueString()), "param3": fmt.Sprintf("%d", config.MapFixed.Param3.ValueInt64())}), + 654: resources_core.FormatString(config.ArrayMapFixedExample), + 863: resources_core.FormatString(config.Arr), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 1, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *DummyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data DummyModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.Arr.IsNull() || data.Arr.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "arr is required.") + return + } + if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 1, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 198: resources_core.FormatInt64(data.DurationMs), + 199: resources_core.FormatBool(data.FailAtStart), + 200: resources_core.FormatBool(data.FailInProgress), + 201: resources_core.FormatInt64(data.WhereFail), + 242: resources_core.FormatString(data.ResourceRealm), + 286: resources_core.FormatString(data.Bodymessage), + 321: resources_core.BuildJSON(map[string]string{"subparam1": fmt.Sprintf("\"%s\"", data.MapExample.Subparam1.ValueString()), "secret": fmt.Sprintf("\"%s\"", data.MapExample.Secret.ValueString()), "subparam2": fmt.Sprintf("\"%s\"", data.MapExample.Subparam2.ValueString())}), + 322: resources_core.FormatString(data.JsonExample), + 396: resources_core.FormatString(data.NestedRefExample), + 647: resources_core.BuildJSON(map[string]string{"bol1": fmt.Sprintf("%v", data.MapFixed.Bol1.ValueBool()), "minStr1": fmt.Sprintf("\"%s\"", data.MapFixed.MinStr1.ValueString()), "param1": fmt.Sprintf("%d", data.MapFixed.Param1.ValueInt64()), "param2": fmt.Sprintf("\"%s\"", data.MapFixed.Param2.ValueString()), "param3": fmt.Sprintf("%d", data.MapFixed.Param3.ValueInt64())}), + 654: resources_core.FormatString(data.ArrayMapFixedExample), + 863: resources_core.FormatString(data.Arr), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 1, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 1, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "arr", Field: "Arr", Type: "string"}, + {Code: "arrayMapFixedExample", Field: "ArrayMapFixedExample", Type: "array-map-fixed"}, + {Code: "bodymessage", Field: "Bodymessage", Type: "string"}, + {Code: "durationMs", Field: "DurationMs", Type: "int64"}, + {Code: "failAtStart", Field: "FailAtStart", Type: "bool"}, + {Code: "failInProgress", Field: "FailInProgress", Type: "bool"}, + {Code: "jsonExample", Field: "JsonExample", Type: "string"}, + {Code: "mapExample", Field: "MapExample", Type: "string"}, + {Code: "mapFixed", Field: "MapFixed", Type: "map-fixed"}, + {Code: "nestedRefExample", Field: "NestedRefExample", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "whereFail", Field: "WhereFail", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *DummyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state DummyModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 1, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "arr", Field: "Arr", Type: "string"}, + {Code: "arrayMapFixedExample", Field: "ArrayMapFixedExample", Type: "array-map-fixed"}, + {Code: "bodymessage", Field: "Bodymessage", Type: "string"}, + {Code: "durationMs", Field: "DurationMs", Type: "int64"}, + {Code: "failAtStart", Field: "FailAtStart", Type: "bool"}, + {Code: "failInProgress", Field: "FailInProgress", Type: "bool"}, + {Code: "jsonExample", Field: "JsonExample", Type: "string"}, + {Code: "mapExample", Field: "MapExample", Type: "string"}, + {Code: "mapFixed", Field: "MapFixed", Type: "map-fixed"}, + {Code: "nestedRefExample", Field: "NestedRefExample", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "whereFail", Field: "WhereFail", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *DummyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan DummyModel + var state DummyModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.DurationMs.IsNull() != state.DurationMs.IsNull() || plan.DurationMs.IsUnknown() != state.DurationMs.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.DurationMs.IsNull() && !plan.DurationMs.IsUnknown() && !state.DurationMs.IsNull() && !state.DurationMs.IsUnknown() { + if plan.DurationMs.ValueInt64() != state.DurationMs.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.FailAtStart.IsNull() != state.FailAtStart.IsNull() || plan.FailAtStart.IsUnknown() != state.FailAtStart.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.FailAtStart.IsNull() && !plan.FailAtStart.IsUnknown() && !state.FailAtStart.IsNull() && !state.FailAtStart.IsUnknown() { + if plan.FailAtStart.ValueBool() != state.FailAtStart.ValueBool() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.FailInProgress.IsNull() != state.FailInProgress.IsNull() || plan.FailInProgress.IsUnknown() != state.FailInProgress.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.FailInProgress.IsNull() && !plan.FailInProgress.IsUnknown() && !state.FailInProgress.IsNull() && !state.FailInProgress.IsUnknown() { + if plan.FailInProgress.ValueBool() != state.FailInProgress.ValueBool() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.WhereFail.IsNull() != state.WhereFail.IsNull() || plan.WhereFail.IsUnknown() != state.WhereFail.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.WhereFail.IsNull() && !plan.WhereFail.IsUnknown() && !state.WhereFail.IsNull() && !state.WhereFail.IsUnknown() { + if plan.WhereFail.ValueInt64() != state.WhereFail.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.Bodymessage.IsNull() != state.Bodymessage.IsNull() || plan.Bodymessage.IsUnknown() != state.Bodymessage.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.Bodymessage.IsNull() && !plan.Bodymessage.IsUnknown() && !state.Bodymessage.IsNull() && !state.Bodymessage.IsUnknown() { + if !strings.EqualFold(plan.Bodymessage.ValueString(), state.Bodymessage.ValueString()) { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.JsonExample.IsNull() != state.JsonExample.IsNull() || plan.JsonExample.IsUnknown() != state.JsonExample.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.JsonExample.IsNull() && !plan.JsonExample.IsUnknown() && !state.JsonExample.IsNull() && !state.JsonExample.IsUnknown() { + if !strings.EqualFold(plan.JsonExample.ValueString(), state.JsonExample.ValueString()) { + hasServiceParamChanges = true + } + } + } + // --- Redeploy support (ARCHITECTURE.md) --- + // Проверяем изменился ли git_revision. + // Если да — вызываем redeploy вместо modify. + // git_revision = "" (не задано) → обычный modify. + redeployRequested := false + if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() { + if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() { + redeployRequested = true + } + } + + if !hasServiceParamChanges && !redeployRequested { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + // --- Redeploy support (ARCHITECTURE.md) --- + if redeployRequested { + // Вызываем redeploy — пустой params (операция не требует CFS-параметров) + if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + } else { + params := map[int]string{ + 287: resources_core.FormatInt64(plan.DurationMs), + 288: resources_core.FormatBool(plan.FailAtStart), + 289: resources_core.FormatBool(plan.FailInProgress), + 290: resources_core.FormatInt64(plan.WhereFail), + 291: resources_core.FormatString(plan.Bodymessage), + 373: resources_core.BuildJSON(map[string]string{"secret": fmt.Sprintf("\"%s\"", plan.MapExample.Secret.ValueString())}), + 374: resources_core.FormatString(plan.JsonExample), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 1, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "arr", Field: "Arr", Type: "string"}, + {Code: "arrayMapFixedExample", Field: "ArrayMapFixedExample", Type: "array-map-fixed"}, + {Code: "bodymessage", Field: "Bodymessage", Type: "string"}, + {Code: "durationMs", Field: "DurationMs", Type: "int64"}, + {Code: "failAtStart", Field: "FailAtStart", Type: "bool"}, + {Code: "failInProgress", Field: "FailInProgress", Type: "bool"}, + {Code: "jsonExample", Field: "JsonExample", Type: "string"}, + {Code: "mapExample", Field: "MapExample", Type: "string"}, + {Code: "mapFixed", Field: "MapFixed", Type: "map-fixed"}, + {Code: "nestedRefExample", Field: "NestedRefExample", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "whereFail", Field: "WhereFail", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *DummyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state DummyModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *DummyResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *DummyResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/20_vc_org_saas_resource.go b/generated/prod/go/20_vc_org_saas_resource.go new file mode 100644 index 0000000..7f53d87 --- /dev/null +++ b/generated/prod/go/20_vc_org_saas_resource.go @@ -0,0 +1,348 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vc_org_saas +// Service ID: 20 + +var _ resource.Resource = &VcOrgSaasResource{} +var _ resource.ResourceWithModifyPlan = &VcOrgSaasResource{} +var _ resource.ResourceWithImportState = &VcOrgSaasResource{} + +type VcOrgSaasResource struct { + client *core.UniversalClient +} + +type VcOrgSaasModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + ResourceRealm types.String `tfsdk:"resource_realm"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewVcOrgSaasResource() resource.Resource { + return &VcOrgSaasResource{} +} + +func (r *VcOrgSaasResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vc_org_saas" +} + +func (r *VcOrgSaasResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("ngcloud.ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VcOrgSaasResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *VcOrgSaasModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *VcOrgSaasModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan VcOrgSaasModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 185: resources_core.FormatString(config.ResourceRealm), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 20, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *VcOrgSaasResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data VcOrgSaasModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 20, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 185: resources_core.FormatString(data.ResourceRealm), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 20, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 20, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcOrgSaasResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VcOrgSaasModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 20, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *VcOrgSaasResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VcOrgSaasModel + var state VcOrgSaasModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 20, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcOrgSaasResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VcOrgSaasModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *VcOrgSaasResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *VcOrgSaasResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/21_vc_vdc_resource.go b/generated/prod/go/21_vc_vdc_resource.go new file mode 100644 index 0000000..9ebce30 --- /dev/null +++ b/generated/prod/go/21_vc_vdc_resource.go @@ -0,0 +1,573 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vc_vdc +// Service ID: 21 + +var _ resource.Resource = &VcVdcResource{} +var _ resource.ResourceWithModifyPlan = &VcVdcResource{} +var _ resource.ResourceWithImportState = &VcVdcResource{} + +type VcVdcResource struct { + client *core.UniversalClient +} + +type VcVdcModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + CpuAllocated types.Int64 `tfsdk:"cpu_allocated"` + CpuGuaranteed types.Int64 `tfsdk:"cpu_guaranteed"` + MemAllocated types.Int64 `tfsdk:"mem_allocated"` + NetworkProvider types.String `tfsdk:"network_provider"` + OrganizationUid types.String `tfsdk:"organization_uid"` + ProviderVdc types.String `tfsdk:"provider_vdc"` + StorageConfig types.String `tfsdk:"storage_config"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewVcVdcResource() resource.Resource { + return &VcVdcResource{} +} + +func (r *VcVdcResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vc_vdc" +} + +func (r *VcVdcResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "cpu_allocated": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "шт"}, + "cpu_guaranteed": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(20), MarkdownDescription: "% резервирования Не изменяется после создания."}, + "mem_allocated": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(20), MarkdownDescription: "gb"}, + "network_provider": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания."}, + "organization_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "Для создания VDC предварительно необходимо создать организацию через услугу `Организация в Cloud Director` Не изменяется после создания."}, + "provider_vdc": schema.StringAttribute{Required: true, MarkdownDescription: "Приписка `2.8` - Скорость vCPU - 2800MHz
И так далее...
Не изменяется после создания."}, + "storage_config": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Первая политика в списке указывается как "По умолчанию""}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VcVdcResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *VcVdcModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *VcVdcModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan VcVdcModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan. + // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов. + // Resolve (displayName → UUID или uppercase → lowercase) нужен только для + // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка + // "Provider produced invalid plan: planned value does not match config value". + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.CpuGuaranteed.IsNull() && !plan.CpuGuaranteed.IsUnknown() && !state.CpuGuaranteed.IsNull() && !state.CpuGuaranteed.IsUnknown() { + if plan.CpuGuaranteed.ValueInt64() != state.CpuGuaranteed.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить cpu_guaranteed", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.NetworkProvider.IsNull() && !plan.NetworkProvider.IsUnknown() && !state.NetworkProvider.IsNull() && !state.NetworkProvider.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.NetworkProvider.ValueString(), state.NetworkProvider.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить network_provider", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.OrganizationUid.IsNull() && !plan.OrganizationUid.IsUnknown() && !state.OrganizationUid.IsNull() && !state.OrganizationUid.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.OrganizationUid.ValueString(), state.OrganizationUid.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить organization_uid", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ProviderVdc.IsNull() && !plan.ProviderVdc.IsUnknown() && !state.ProviderVdc.IsNull() && !state.ProviderVdc.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ProviderVdc.ValueString(), state.ProviderVdc.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить provider_vdc", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.NetworkProvider.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "network_provider is required.") + return + } + if config.NetworkProvider.IsUnknown() { + return + } + if config.OrganizationUid.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "organization_uid is required.") + return + } + if config.OrganizationUid.IsUnknown() { + return + } + if config.ProviderVdc.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "provider_vdc is required.") + return + } + if config.ProviderVdc.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + if !config.OrganizationUid.IsNull() && !config.OrganizationUid.IsUnknown() { + resolvedOrganizationUid, err := r.client.ResolveRefSvcParamValue(ctx, 19, config.OrganizationUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve organization_uid", err.Error()) + } else if resolvedOrganizationUid != "" && resolvedOrganizationUid != config.OrganizationUid.ValueString() { + config.OrganizationUid = types.StringValue(resolvedOrganizationUid) + } + } + params := map[int]string{ + 30: resources_core.FormatString(config.OrganizationUid), + 335: resources_core.FormatString(config.ProviderVdc), + 361: resources_core.FormatString(config.StorageConfig), + 397: resources_core.FormatInt64(config.CpuGuaranteed), + 557: resources_core.FormatInt64(config.CpuAllocated), + 558: resources_core.FormatInt64(config.MemAllocated), + 746: resources_core.FormatString(config.NetworkProvider), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 21, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *VcVdcResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data VcVdcModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.NetworkProvider.IsNull() || data.NetworkProvider.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "network_provider is required.") + return + } + if data.OrganizationUid.IsNull() || data.OrganizationUid.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "organization_uid is required.") + return + } + if data.ProviderVdc.IsNull() || data.ProviderVdc.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "provider_vdc is required.") + return + } + // ══════════════════════════════════════════════════════════════════════════ + // ПРАВИЛО TERRAFORM (официальная документация): + // «If an attribute value is configured, it is NEVER valid to change that + // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что + // написал пользователь). Менять plan запрещено на уровне фреймворка. + // + // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре. + // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный). + // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит: + // «Provider produced inconsistent result after apply». + // + // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот. + // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как + // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API). + // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный + // регистр в state через strings.EqualFold (сравниваем без учёта регистра). + // ══════════════════════════════════════════════════════════════════════════ + originalOrganizationUid := data.OrganizationUid + if !data.OrganizationUid.IsNull() && !data.OrganizationUid.IsUnknown() { + resolvedOrganizationUid, err := r.client.ResolveRefSvcParamValue(ctx, 19, data.OrganizationUid.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if resolvedOrganizationUid != "" && resolvedOrganizationUid != data.OrganizationUid.ValueString() { + data.OrganizationUid = types.StringValue(resolvedOrganizationUid) + } + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 21, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 30: resources_core.FormatString(data.OrganizationUid), + 335: resources_core.FormatString(data.ProviderVdc), + 361: resources_core.FormatString(data.StorageConfig), + 397: resources_core.FormatInt64(data.CpuGuaranteed), + 557: resources_core.FormatInt64(data.CpuAllocated), + 558: resources_core.FormatInt64(data.MemAllocated), + 746: resources_core.FormatString(data.NetworkProvider), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 21, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 21, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "cpuAllocated", Field: "CpuAllocated", Type: "int64"}, + {Code: "cpuGuaranteed", Field: "CpuGuaranteed", Type: "int64"}, + {Code: "memAllocated", Field: "MemAllocated", Type: "int64"}, + {Code: "networkProvider", Field: "NetworkProvider", Type: "string"}, + {Code: "organizationUid", Field: "OrganizationUid", Type: "string"}, + {Code: "providerVdc", Field: "ProviderVdc", Type: "string"}, + {Code: "storageConfig", Field: "StorageConfig", Type: "array-map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.OrganizationUid.IsNull() && !state.OrganizationUid.IsUnknown() { + resolvedOrganizationUid, err := r.client.ResolveRefSvcParamValue(ctx, 19, state.OrganizationUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve organization_uid", err.Error()) + } else if resolvedOrganizationUid != "" && resolvedOrganizationUid != state.OrganizationUid.ValueString() { + state.OrganizationUid = types.StringValue(resolvedOrganizationUid) + } + } + // Restore user-provided casing в state (Create). + // EqualFold = «совпадают ли значения без учёта регистра?» + // Если да — значит API вернул «туже» строку, только в другом регистре. + // Заменяем state на original (то что было в plan/config пользователя). + // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint. + if !originalOrganizationUid.IsNull() && !originalOrganizationUid.IsUnknown() && !state.OrganizationUid.IsNull() && !state.OrganizationUid.IsUnknown() { + if strings.EqualFold(state.OrganizationUid.ValueString(), originalOrganizationUid.ValueString()) { + state.OrganizationUid = originalOrganizationUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcVdcResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VcVdcModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 21, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "cpuAllocated", Field: "CpuAllocated", Type: "int64"}, + {Code: "cpuGuaranteed", Field: "CpuGuaranteed", Type: "int64"}, + {Code: "memAllocated", Field: "MemAllocated", Type: "int64"}, + {Code: "networkProvider", Field: "NetworkProvider", Type: "string"}, + {Code: "organizationUid", Field: "OrganizationUid", Type: "string"}, + {Code: "providerVdc", Field: "ProviderVdc", Type: "string"}, + {Code: "storageConfig", Field: "StorageConfig", Type: "array-map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !newState.OrganizationUid.IsNull() && !newState.OrganizationUid.IsUnknown() { + resolvedOrganizationUid, err := r.client.ResolveRefSvcParamValue(ctx, 19, newState.OrganizationUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve organization_uid", err.Error()) + } else if resolvedOrganizationUid != "" && resolvedOrganizationUid != newState.OrganizationUid.ValueString() { + newState.OrganizationUid = types.StringValue(resolvedOrganizationUid) + } + } + // Restore user-provided casing в state (Read). + // При чтении у нас нет plan — но предыдущий state уже хранит значение + // в регистре пользователя (после первого Create оно было восстановлено). + // Берём prior state (переменная state) как эталон регистра. + // Если API вернул то же UUID только строчными буквами — восстанавливаем. + if !state.OrganizationUid.IsNull() && !state.OrganizationUid.IsUnknown() && !newState.OrganizationUid.IsNull() && !newState.OrganizationUid.IsUnknown() { + if strings.EqualFold(newState.OrganizationUid.ValueString(), state.OrganizationUid.ValueString()) { + newState.OrganizationUid = state.OrganizationUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *VcVdcResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VcVdcModel + var state VcVdcModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.CpuAllocated.IsNull() != state.CpuAllocated.IsNull() || plan.CpuAllocated.IsUnknown() != state.CpuAllocated.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.CpuAllocated.IsNull() && !plan.CpuAllocated.IsUnknown() && !state.CpuAllocated.IsNull() && !state.CpuAllocated.IsUnknown() { + if plan.CpuAllocated.ValueInt64() != state.CpuAllocated.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.MemAllocated.IsNull() != state.MemAllocated.IsNull() || plan.MemAllocated.IsUnknown() != state.MemAllocated.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.MemAllocated.IsNull() && !plan.MemAllocated.IsUnknown() && !state.MemAllocated.IsNull() && !state.MemAllocated.IsUnknown() { + if plan.MemAllocated.ValueInt64() != state.MemAllocated.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.StorageConfig.IsNull() != state.StorageConfig.IsNull() || plan.StorageConfig.IsUnknown() != state.StorageConfig.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.StorageConfig.IsNull() && !plan.StorageConfig.IsUnknown() && !state.StorageConfig.IsNull() && !state.StorageConfig.IsUnknown() { + if !strings.EqualFold(plan.StorageConfig.ValueString(), state.StorageConfig.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 560: resources_core.FormatInt64(plan.CpuAllocated), + 561: resources_core.FormatInt64(plan.MemAllocated), + 562: resources_core.FormatString(plan.StorageConfig), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 21, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "cpuAllocated", Field: "CpuAllocated", Type: "int64"}, + {Code: "cpuGuaranteed", Field: "CpuGuaranteed", Type: "int64"}, + {Code: "memAllocated", Field: "MemAllocated", Type: "int64"}, + {Code: "networkProvider", Field: "NetworkProvider", Type: "string"}, + {Code: "organizationUid", Field: "OrganizationUid", Type: "string"}, + {Code: "providerVdc", Field: "ProviderVdc", Type: "string"}, + {Code: "storageConfig", Field: "StorageConfig", Type: "array-map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.OrganizationUid.IsNull() && !state.OrganizationUid.IsUnknown() { + resolvedOrganizationUid, err := r.client.ResolveRefSvcParamValue(ctx, 19, state.OrganizationUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve organization_uid", err.Error()) + } else if resolvedOrganizationUid != "" && resolvedOrganizationUid != state.OrganizationUid.ValueString() { + state.OrganizationUid = types.StringValue(resolvedOrganizationUid) + } + } + // Restore user-provided casing в state (Update). + // После API-вызова modify state содержит значения в lower-case от API. + // Plan == config == то что написал пользователь (регистр неизменён). + // EqualFold: если UUID совпадает без учёта регистра — берём из plan. + if !plan.OrganizationUid.IsNull() && !plan.OrganizationUid.IsUnknown() && !state.OrganizationUid.IsNull() && !state.OrganizationUid.IsUnknown() { + if strings.EqualFold(state.OrganizationUid.ValueString(), plan.OrganizationUid.ValueString()) { + state.OrganizationUid = plan.OrganizationUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcVdcResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VcVdcModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *VcVdcResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *VcVdcResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/22_vc_nsxt_resource.go b/generated/prod/go/22_vc_nsxt_resource.go new file mode 100644 index 0000000..443f3a6 --- /dev/null +++ b/generated/prod/go/22_vc_nsxt_resource.go @@ -0,0 +1,614 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vc_nsxt +// Service ID: 22 + +var _ resource.Resource = &VcNsxtResource{} +var _ resource.ResourceWithModifyPlan = &VcNsxtResource{} +var _ resource.ResourceWithImportState = &VcNsxtResource{} + +type VcNsxtResource struct { + client *core.UniversalClient +} + +type VcNsxtModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + IpSpaceName types.String `tfsdk:"ip_space_name"` + NeedEnableAVI types.Bool `tfsdk:"need_enable_a_v_i"` + QosProfile types.String `tfsdk:"qos_profile"` + VdcGroupUid types.String `tfsdk:"vdc_group_uid"` + VdcType types.String `tfsdk:"vdc_type"` + VdcUid types.String `tfsdk:"vdc_uid"` + VirtualServicesCount types.Int64 `tfsdk:"virtual_services_count"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewVcNsxtResource() resource.Resource { + return &VcNsxtResource{} +} + +func (r *VcNsxtResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vc_nsxt" +} + +func (r *VcNsxtResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "ip_space_name": schema.StringAttribute{Optional: true, MarkdownDescription: "Имя ip Space для внешнего IP"}, + "need_enable_a_v_i": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включение Load Balancer"}, + "qos_profile": schema.StringAttribute{Optional: true, MarkdownDescription: "Параметр пока не работает
Должен возвращать поле из ресурсной платформы типа vc из .vcd.hardware.gatewayQoSProfiles

Как по идее должен работать.
Нужно заполнить или CFS vdcUid, или vdcGroupUid
Идеально конечно проверять `if (params.vdcType == "vdc" && params.vdcUid != "")` или `if (params.vdcType == "vdcGroup" && params.vdcGroupUid != "")`

Далее надо пойти по стейту vdc -> org -> resPlatform, взять gatewayQoSProfiles
Или пойти по стейту vdcgroup -> vdc -> org -> resPlatform, взять gatewayQoSProfiles

Регулярку могу написать (Виталя)"}, + "vdc_group_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UUID Услуги `Группа виртуальных датацентров (groupvDC)` Не изменяется после создания."}, + "vdc_type": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("vdc"), MarkdownDescription: "Тип родительской услуги Не изменяется после создания."}, + "vdc_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UUID Услуги `Виртуальный датацентр (vDC)` Не изменяется после создания."}, + "virtual_services_count": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Кол-во виртуальных сервисов, которые **резервируются** на AVI"}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VcNsxtResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *VcNsxtModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *VcNsxtModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan VcNsxtModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan. + // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов. + // Resolve (displayName → UUID или uppercase → lowercase) нужен только для + // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка + // "Provider produced invalid plan: planned value does not match config value". + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.VdcGroupUid.IsNull() && !plan.VdcGroupUid.IsUnknown() && !state.VdcGroupUid.IsNull() && !state.VdcGroupUid.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VdcGroupUid.ValueString(), state.VdcGroupUid.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить vdc_group_uid", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.VdcType.IsNull() && !plan.VdcType.IsUnknown() && !state.VdcType.IsNull() && !state.VdcType.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VdcType.ValueString(), state.VdcType.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить vdc_type", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.VdcUid.IsNull() && !plan.VdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VdcUid.ValueString(), state.VdcUid.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить vdc_uid", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + if !config.VdcGroupUid.IsNull() && !config.VdcGroupUid.IsUnknown() { + resolvedVdcGroupUid, err := r.client.ResolveRefSvcParamValue(ctx, 29, config.VdcGroupUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_group_uid", err.Error()) + } else if resolvedVdcGroupUid != "" && resolvedVdcGroupUid != config.VdcGroupUid.ValueString() { + config.VdcGroupUid = types.StringValue(resolvedVdcGroupUid) + } + } + if !config.VdcUid.IsNull() && !config.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, config.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error()) + } else if resolvedVdcUid != "" && resolvedVdcUid != config.VdcUid.ValueString() { + config.VdcUid = types.StringValue(resolvedVdcUid) + } + } + params := map[int]string{ + 8: resources_core.FormatString(config.VdcUid), + 340: resources_core.FormatBool(config.NeedEnableAVI), + 341: resources_core.FormatInt64(config.VirtualServicesCount), + 621: resources_core.FormatString(config.VdcType), + 622: resources_core.FormatString(config.VdcGroupUid), + 825: resources_core.FormatString(config.QosProfile), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 22, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *VcNsxtResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data VcNsxtModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + // ══════════════════════════════════════════════════════════════════════════ + // ПРАВИЛО TERRAFORM (официальная документация): + // «If an attribute value is configured, it is NEVER valid to change that + // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что + // написал пользователь). Менять plan запрещено на уровне фреймворка. + // + // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре. + // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный). + // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит: + // «Provider produced inconsistent result after apply». + // + // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот. + // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как + // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API). + // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный + // регистр в state через strings.EqualFold (сравниваем без учёта регистра). + // ══════════════════════════════════════════════════════════════════════════ + originalVdcGroupUid := data.VdcGroupUid + // ══════════════════════════════════════════════════════════════════════════ + // ПРАВИЛО TERRAFORM (официальная документация): + // «If an attribute value is configured, it is NEVER valid to change that + // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что + // написал пользователь). Менять plan запрещено на уровне фреймворка. + // + // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре. + // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный). + // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит: + // «Provider produced inconsistent result after apply». + // + // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот. + // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как + // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API). + // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный + // регистр в state через strings.EqualFold (сравниваем без учёта регистра). + // ══════════════════════════════════════════════════════════════════════════ + originalVdcUid := data.VdcUid + if !data.VdcGroupUid.IsNull() && !data.VdcGroupUid.IsUnknown() { + resolvedVdcGroupUid, err := r.client.ResolveRefSvcParamValue(ctx, 29, data.VdcGroupUid.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if resolvedVdcGroupUid != "" && resolvedVdcGroupUid != data.VdcGroupUid.ValueString() { + data.VdcGroupUid = types.StringValue(resolvedVdcGroupUid) + } + } + if !data.VdcUid.IsNull() && !data.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, data.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if resolvedVdcUid != "" && resolvedVdcUid != data.VdcUid.ValueString() { + data.VdcUid = types.StringValue(resolvedVdcUid) + } + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 22, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 8: resources_core.FormatString(data.VdcUid), + 340: resources_core.FormatBool(data.NeedEnableAVI), + 341: resources_core.FormatInt64(data.VirtualServicesCount), + 621: resources_core.FormatString(data.VdcType), + 622: resources_core.FormatString(data.VdcGroupUid), + 825: resources_core.FormatString(data.QosProfile), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 22, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 22, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "needEnableAVI", Field: "NeedEnableAVI", Type: "bool"}, + {Code: "qosProfile", Field: "QosProfile", Type: "string"}, + {Code: "vdcGroupUid", Field: "VdcGroupUid", Type: "string"}, + {Code: "vdcType", Field: "VdcType", Type: "string"}, + {Code: "vdcUid", Field: "VdcUid", Type: "string"}, + {Code: "virtualServicesCount", Field: "VirtualServicesCount", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.VdcGroupUid.IsNull() && !state.VdcGroupUid.IsUnknown() { + resolvedVdcGroupUid, err := r.client.ResolveRefSvcParamValue(ctx, 29, state.VdcGroupUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_group_uid", err.Error()) + } else if resolvedVdcGroupUid != "" && resolvedVdcGroupUid != state.VdcGroupUid.ValueString() { + state.VdcGroupUid = types.StringValue(resolvedVdcGroupUid) + } + } + if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, state.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error()) + } else if resolvedVdcUid != "" && resolvedVdcUid != state.VdcUid.ValueString() { + state.VdcUid = types.StringValue(resolvedVdcUid) + } + } + // Restore user-provided casing в state (Create). + // EqualFold = «совпадают ли значения без учёта регистра?» + // Если да — значит API вернул «туже» строку, только в другом регистре. + // Заменяем state на original (то что было в plan/config пользователя). + // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint. + if !originalVdcGroupUid.IsNull() && !originalVdcGroupUid.IsUnknown() && !state.VdcGroupUid.IsNull() && !state.VdcGroupUid.IsUnknown() { + if strings.EqualFold(state.VdcGroupUid.ValueString(), originalVdcGroupUid.ValueString()) { + state.VdcGroupUid = originalVdcGroupUid + } + } + if !originalVdcUid.IsNull() && !originalVdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + if strings.EqualFold(state.VdcUid.ValueString(), originalVdcUid.ValueString()) { + state.VdcUid = originalVdcUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcNsxtResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VcNsxtModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 22, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "needEnableAVI", Field: "NeedEnableAVI", Type: "bool"}, + {Code: "qosProfile", Field: "QosProfile", Type: "string"}, + {Code: "vdcGroupUid", Field: "VdcGroupUid", Type: "string"}, + {Code: "vdcType", Field: "VdcType", Type: "string"}, + {Code: "vdcUid", Field: "VdcUid", Type: "string"}, + {Code: "virtualServicesCount", Field: "VirtualServicesCount", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !newState.VdcGroupUid.IsNull() && !newState.VdcGroupUid.IsUnknown() { + resolvedVdcGroupUid, err := r.client.ResolveRefSvcParamValue(ctx, 29, newState.VdcGroupUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_group_uid", err.Error()) + } else if resolvedVdcGroupUid != "" && resolvedVdcGroupUid != newState.VdcGroupUid.ValueString() { + newState.VdcGroupUid = types.StringValue(resolvedVdcGroupUid) + } + } + if !newState.VdcUid.IsNull() && !newState.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, newState.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error()) + } else if resolvedVdcUid != "" && resolvedVdcUid != newState.VdcUid.ValueString() { + newState.VdcUid = types.StringValue(resolvedVdcUid) + } + } + // Restore user-provided casing в state (Read). + // При чтении у нас нет plan — но предыдущий state уже хранит значение + // в регистре пользователя (после первого Create оно было восстановлено). + // Берём prior state (переменная state) как эталон регистра. + // Если API вернул то же UUID только строчными буквами — восстанавливаем. + if !state.VdcGroupUid.IsNull() && !state.VdcGroupUid.IsUnknown() && !newState.VdcGroupUid.IsNull() && !newState.VdcGroupUid.IsUnknown() { + if strings.EqualFold(newState.VdcGroupUid.ValueString(), state.VdcGroupUid.ValueString()) { + newState.VdcGroupUid = state.VdcGroupUid + } + } + if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() && !newState.VdcUid.IsNull() && !newState.VdcUid.IsUnknown() { + if strings.EqualFold(newState.VdcUid.ValueString(), state.VdcUid.ValueString()) { + newState.VdcUid = state.VdcUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *VcNsxtResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VcNsxtModel + var state VcNsxtModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.NeedEnableAVI.IsNull() != state.NeedEnableAVI.IsNull() || plan.NeedEnableAVI.IsUnknown() != state.NeedEnableAVI.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.NeedEnableAVI.IsNull() && !plan.NeedEnableAVI.IsUnknown() && !state.NeedEnableAVI.IsNull() && !state.NeedEnableAVI.IsUnknown() { + if plan.NeedEnableAVI.ValueBool() != state.NeedEnableAVI.ValueBool() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.VirtualServicesCount.IsNull() != state.VirtualServicesCount.IsNull() || plan.VirtualServicesCount.IsUnknown() != state.VirtualServicesCount.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.VirtualServicesCount.IsNull() && !plan.VirtualServicesCount.IsUnknown() && !state.VirtualServicesCount.IsNull() && !state.VirtualServicesCount.IsUnknown() { + if plan.VirtualServicesCount.ValueInt64() != state.VirtualServicesCount.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.IpSpaceName.IsNull() != state.IpSpaceName.IsNull() || plan.IpSpaceName.IsUnknown() != state.IpSpaceName.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.IpSpaceName.IsNull() && !plan.IpSpaceName.IsUnknown() && !state.IpSpaceName.IsNull() && !state.IpSpaceName.IsUnknown() { + if !strings.EqualFold(plan.IpSpaceName.ValueString(), state.IpSpaceName.ValueString()) { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.QosProfile.IsNull() != state.QosProfile.IsNull() || plan.QosProfile.IsUnknown() != state.QosProfile.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.QosProfile.IsNull() && !plan.QosProfile.IsUnknown() && !state.QosProfile.IsNull() && !state.QosProfile.IsUnknown() { + if !strings.EqualFold(plan.QosProfile.ValueString(), state.QosProfile.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 368: resources_core.FormatBool(plan.NeedEnableAVI), + 369: resources_core.FormatInt64(plan.VirtualServicesCount), + 372: resources_core.FormatString(plan.IpSpaceName), + 856: resources_core.FormatString(plan.QosProfile), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 22, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "needEnableAVI", Field: "NeedEnableAVI", Type: "bool"}, + {Code: "qosProfile", Field: "QosProfile", Type: "string"}, + {Code: "vdcGroupUid", Field: "VdcGroupUid", Type: "string"}, + {Code: "vdcType", Field: "VdcType", Type: "string"}, + {Code: "vdcUid", Field: "VdcUid", Type: "string"}, + {Code: "virtualServicesCount", Field: "VirtualServicesCount", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.VdcGroupUid.IsNull() && !state.VdcGroupUid.IsUnknown() { + resolvedVdcGroupUid, err := r.client.ResolveRefSvcParamValue(ctx, 29, state.VdcGroupUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_group_uid", err.Error()) + } else if resolvedVdcGroupUid != "" && resolvedVdcGroupUid != state.VdcGroupUid.ValueString() { + state.VdcGroupUid = types.StringValue(resolvedVdcGroupUid) + } + } + if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, state.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error()) + } else if resolvedVdcUid != "" && resolvedVdcUid != state.VdcUid.ValueString() { + state.VdcUid = types.StringValue(resolvedVdcUid) + } + } + // Restore user-provided casing в state (Update). + // После API-вызова modify state содержит значения в lower-case от API. + // Plan == config == то что написал пользователь (регистр неизменён). + // EqualFold: если UUID совпадает без учёта регистра — берём из plan. + if !plan.VdcGroupUid.IsNull() && !plan.VdcGroupUid.IsUnknown() && !state.VdcGroupUid.IsNull() && !state.VdcGroupUid.IsUnknown() { + if strings.EqualFold(state.VdcGroupUid.ValueString(), plan.VdcGroupUid.ValueString()) { + state.VdcGroupUid = plan.VdcGroupUid + } + } + if !plan.VdcUid.IsNull() && !plan.VdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + if strings.EqualFold(state.VdcUid.ValueString(), plan.VdcUid.ValueString()) { + state.VdcUid = plan.VdcUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcNsxtResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VcNsxtModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *VcNsxtResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *VcNsxtResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/23_vc_vm_resource.go b/generated/prod/go/23_vc_vm_resource.go new file mode 100644 index 0000000..ff0b8c6 --- /dev/null +++ b/generated/prod/go/23_vc_vm_resource.go @@ -0,0 +1,581 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vc_vm +// Service ID: 23 + +var _ resource.Resource = &VcVmResource{} +var _ resource.ResourceWithModifyPlan = &VcVmResource{} +var _ resource.ResourceWithImportState = &VcVmResource{} + +type VcVmResource struct { + client *core.UniversalClient +} + +type VcVmModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessIpList types.String `tfsdk:"access_ip_list"` + IpSpaceName types.String `tfsdk:"ip_space_name"` + TemplateName types.String `tfsdk:"template_name"` + VappUid types.String `tfsdk:"vapp_uid"` + VmCpu types.String `tfsdk:"vm_cpu"` + VmDisksize types.String `tfsdk:"vm_disksize"` + VmName types.String `tfsdk:"vm_name"` + VmRam types.String `tfsdk:"vm_ram"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewVcVmResource() resource.Resource { + return &VcVmResource{} +} + +func (r *VcVmResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vc_vm" +} + +func (r *VcVmResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_ip_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("79.111.119.78/32 185.247.187.1/32"), MarkdownDescription: "Вставить содержимое из 'curl ifconfig.io'
IP адреса в CIDR формате через пробел для настройки аксес листов"}, + "ip_space_name": schema.StringAttribute{Required: true, MarkdownDescription: "Брать из свойств ресурсной платформы
.vcd.ipSpace

Имя IP-space для выделения внешнего IP (IP выделяется обязательно) Не изменяется после создания."}, + "template_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("RockyLinux_9-16G"), MarkdownDescription: "ОС Не изменяется после создания."}, + "vapp_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "InstanceUid vApp. vApp должен быть создан Не изменяется после создания."}, + "vm_cpu": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("2"), MarkdownDescription: "CPU"}, + "vm_disksize": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("10"), MarkdownDescription: "Размер диска под сервис (GB)"}, + "vm_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("vm0001"), MarkdownDescription: "Имя ВМ. Не поддерживает спец-символы Не изменяется после создания."}, + "vm_ram": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("2"), MarkdownDescription: "RAM (GB)"}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VcVmResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *VcVmModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *VcVmModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan VcVmModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan. + // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов. + // Resolve (displayName → UUID или uppercase → lowercase) нужен только для + // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка + // "Provider produced invalid plan: planned value does not match config value". + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.IpSpaceName.IsNull() && !plan.IpSpaceName.IsUnknown() && !state.IpSpaceName.IsNull() && !state.IpSpaceName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.IpSpaceName.ValueString(), state.IpSpaceName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить ip_space_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.TemplateName.IsNull() && !plan.TemplateName.IsUnknown() && !state.TemplateName.IsNull() && !state.TemplateName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.TemplateName.ValueString(), state.TemplateName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить template_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.VappUid.IsNull() && !plan.VappUid.IsUnknown() && !state.VappUid.IsNull() && !state.VappUid.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VappUid.ValueString(), state.VappUid.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить vapp_uid", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.VmName.IsNull() && !plan.VmName.IsUnknown() && !state.VmName.IsNull() && !state.VmName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VmName.ValueString(), state.VmName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить vm_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.IpSpaceName.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "ip_space_name is required.") + return + } + if config.IpSpaceName.IsUnknown() { + return + } + if config.VappUid.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "vapp_uid is required.") + return + } + if config.VappUid.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + if !config.VappUid.IsNull() && !config.VappUid.IsUnknown() { + resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, config.VappUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error()) + } else if resolvedVappUid != "" && resolvedVappUid != config.VappUid.ValueString() { + config.VappUid = types.StringValue(resolvedVappUid) + } + } + params := map[int]string{ + 33: resources_core.FormatString(config.VappUid), + 35: resources_core.FormatString(config.VmName), + 36: resources_core.FormatString(config.VmCpu), + 37: resources_core.FormatString(config.VmRam), + 38: resources_core.FormatString(config.VmDisksize), + 176: resources_core.FormatString(config.TemplateName), + 177: resources_core.FormatString(config.AccessIpList), + 406: resources_core.FormatString(config.IpSpaceName), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 23, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *VcVmResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data VcVmModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.IpSpaceName.IsNull() || data.IpSpaceName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "ip_space_name is required.") + return + } + if data.VappUid.IsNull() || data.VappUid.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "vapp_uid is required.") + return + } + // ══════════════════════════════════════════════════════════════════════════ + // ПРАВИЛО TERRAFORM (официальная документация): + // «If an attribute value is configured, it is NEVER valid to change that + // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что + // написал пользователь). Менять plan запрещено на уровне фреймворка. + // + // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре. + // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный). + // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит: + // «Provider produced inconsistent result after apply». + // + // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот. + // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как + // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API). + // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный + // регистр в state через strings.EqualFold (сравниваем без учёта регистра). + // ══════════════════════════════════════════════════════════════════════════ + originalVappUid := data.VappUid + if !data.VappUid.IsNull() && !data.VappUid.IsUnknown() { + resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, data.VappUid.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if resolvedVappUid != "" && resolvedVappUid != data.VappUid.ValueString() { + data.VappUid = types.StringValue(resolvedVappUid) + } + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 23, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 33: resources_core.FormatString(data.VappUid), + 35: resources_core.FormatString(data.VmName), + 36: resources_core.FormatString(data.VmCpu), + 37: resources_core.FormatString(data.VmRam), + 38: resources_core.FormatString(data.VmDisksize), + 176: resources_core.FormatString(data.TemplateName), + 177: resources_core.FormatString(data.AccessIpList), + 406: resources_core.FormatString(data.IpSpaceName), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 23, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 23, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessIpList", Field: "AccessIpList", Type: "string"}, + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "templateName", Field: "TemplateName", Type: "string"}, + {Code: "vappUid", Field: "VappUid", Type: "string"}, + {Code: "vmCpu", Field: "VmCpu", Type: "string"}, + {Code: "vmDisksize", Field: "VmDisksize", Type: "string"}, + {Code: "vmName", Field: "VmName", Type: "string"}, + {Code: "vmRam", Field: "VmRam", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.VappUid.IsNull() && !state.VappUid.IsUnknown() { + resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, state.VappUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error()) + } else if resolvedVappUid != "" && resolvedVappUid != state.VappUid.ValueString() { + state.VappUid = types.StringValue(resolvedVappUid) + } + } + // Restore user-provided casing в state (Create). + // EqualFold = «совпадают ли значения без учёта регистра?» + // Если да — значит API вернул «туже» строку, только в другом регистре. + // Заменяем state на original (то что было в plan/config пользователя). + // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint. + if !originalVappUid.IsNull() && !originalVappUid.IsUnknown() && !state.VappUid.IsNull() && !state.VappUid.IsUnknown() { + if strings.EqualFold(state.VappUid.ValueString(), originalVappUid.ValueString()) { + state.VappUid = originalVappUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcVmResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VcVmModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 23, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessIpList", Field: "AccessIpList", Type: "string"}, + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "templateName", Field: "TemplateName", Type: "string"}, + {Code: "vappUid", Field: "VappUid", Type: "string"}, + {Code: "vmCpu", Field: "VmCpu", Type: "string"}, + {Code: "vmDisksize", Field: "VmDisksize", Type: "string"}, + {Code: "vmName", Field: "VmName", Type: "string"}, + {Code: "vmRam", Field: "VmRam", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !newState.VappUid.IsNull() && !newState.VappUid.IsUnknown() { + resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, newState.VappUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error()) + } else if resolvedVappUid != "" && resolvedVappUid != newState.VappUid.ValueString() { + newState.VappUid = types.StringValue(resolvedVappUid) + } + } + // Restore user-provided casing в state (Read). + // При чтении у нас нет plan — но предыдущий state уже хранит значение + // в регистре пользователя (после первого Create оно было восстановлено). + // Берём prior state (переменная state) как эталон регистра. + // Если API вернул то же UUID только строчными буквами — восстанавливаем. + if !state.VappUid.IsNull() && !state.VappUid.IsUnknown() && !newState.VappUid.IsNull() && !newState.VappUid.IsUnknown() { + if strings.EqualFold(newState.VappUid.ValueString(), state.VappUid.ValueString()) { + newState.VappUid = state.VappUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *VcVmResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VcVmModel + var state VcVmModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.VmCpu.IsNull() != state.VmCpu.IsNull() || plan.VmCpu.IsUnknown() != state.VmCpu.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.VmCpu.IsNull() && !plan.VmCpu.IsUnknown() && !state.VmCpu.IsNull() && !state.VmCpu.IsUnknown() { + if !strings.EqualFold(plan.VmCpu.ValueString(), state.VmCpu.ValueString()) { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.VmRam.IsNull() != state.VmRam.IsNull() || plan.VmRam.IsUnknown() != state.VmRam.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.VmRam.IsNull() && !plan.VmRam.IsUnknown() && !state.VmRam.IsNull() && !state.VmRam.IsUnknown() { + if !strings.EqualFold(plan.VmRam.ValueString(), state.VmRam.ValueString()) { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.VmDisksize.IsNull() != state.VmDisksize.IsNull() || plan.VmDisksize.IsUnknown() != state.VmDisksize.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.VmDisksize.IsNull() && !plan.VmDisksize.IsUnknown() && !state.VmDisksize.IsNull() && !state.VmDisksize.IsUnknown() { + if !strings.EqualFold(plan.VmDisksize.ValueString(), state.VmDisksize.ValueString()) { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.AccessIpList.IsNull() != state.AccessIpList.IsNull() || plan.AccessIpList.IsUnknown() != state.AccessIpList.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.AccessIpList.IsNull() && !plan.AccessIpList.IsUnknown() && !state.AccessIpList.IsNull() && !state.AccessIpList.IsUnknown() { + if !strings.EqualFold(plan.AccessIpList.ValueString(), state.AccessIpList.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 186: resources_core.FormatString(plan.VmCpu), + 187: resources_core.FormatString(plan.VmRam), + 188: resources_core.FormatString(plan.VmDisksize), + 189: resources_core.FormatString(plan.AccessIpList), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 23, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessIpList", Field: "AccessIpList", Type: "string"}, + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "templateName", Field: "TemplateName", Type: "string"}, + {Code: "vappUid", Field: "VappUid", Type: "string"}, + {Code: "vmCpu", Field: "VmCpu", Type: "string"}, + {Code: "vmDisksize", Field: "VmDisksize", Type: "string"}, + {Code: "vmName", Field: "VmName", Type: "string"}, + {Code: "vmRam", Field: "VmRam", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.VappUid.IsNull() && !state.VappUid.IsUnknown() { + resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, state.VappUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error()) + } else if resolvedVappUid != "" && resolvedVappUid != state.VappUid.ValueString() { + state.VappUid = types.StringValue(resolvedVappUid) + } + } + // Restore user-provided casing в state (Update). + // После API-вызова modify state содержит значения в lower-case от API. + // Plan == config == то что написал пользователь (регистр неизменён). + // EqualFold: если UUID совпадает без учёта регистра — берём из plan. + if !plan.VappUid.IsNull() && !plan.VappUid.IsUnknown() && !state.VappUid.IsNull() && !state.VappUid.IsUnknown() { + if strings.EqualFold(state.VappUid.ValueString(), plan.VappUid.ValueString()) { + state.VappUid = plan.VappUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcVmResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VcVmModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *VcVmResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *VcVmResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/25_vcexternalip_resource.go b/generated/prod/go/25_vcexternalip_resource.go new file mode 100644 index 0000000..d11a84c --- /dev/null +++ b/generated/prod/go/25_vcexternalip_resource.go @@ -0,0 +1,604 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vcexternalip +// Service ID: 25 + +var _ resource.Resource = &VcexternalipResource{} +var _ resource.ResourceWithModifyPlan = &VcexternalipResource{} +var _ resource.ResourceWithImportState = &VcexternalipResource{} + +type VcexternalipResource struct { + client *core.UniversalClient +} + +type VcexternalipModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + DnatCreate types.Bool `tfsdk:"dnat_create"` + FromServiceCloudEdgeName types.String `tfsdk:"from_service_cloud_edge_name"` + FromServiceCloudEdgeScope types.String `tfsdk:"from_service_cloud_edge_scope"` + FromServiceCloudOrgName types.String `tfsdk:"from_service_cloud_org_name"` + FromServiceCloudVdcName types.String `tfsdk:"from_service_cloud_vdc_name"` + FromServiceCloudVmwareUrl types.String `tfsdk:"from_service_cloud_vmware_url"` + FromServiceNamespace types.String `tfsdk:"from_service_namespace"` + FromServiceVdcGroupName types.String `tfsdk:"from_service_vdc_group_name"` + InternalAddrAccess types.String `tfsdk:"internal_addr_access"` + InternalPortAccess types.String `tfsdk:"internal_port_access"` + IpSpaceName types.String `tfsdk:"ip_space_name"` + ResourceRealm types.String `tfsdk:"resource_realm"` + ServiceUid types.String `tfsdk:"service_uid"` + SnatCreate types.Bool `tfsdk:"snat_create"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewVcexternalipResource() resource.Resource { + return &VcexternalipResource{} +} + +func (r *VcexternalipResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vcexternalip" +} + +func (r *VcexternalipResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "dnat_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Необходимо создать DNAT-правило."}, + "from_service_cloud_edge_name": schema.StringAttribute{Optional: true, MarkdownDescription: "Edge составной услуги (указывать, если resourceRealm связан с CloudDirector) Не изменяется после создания."}, + "from_service_cloud_edge_scope": schema.StringAttribute{Optional: true, MarkdownDescription: "Не изменяется после создания."}, + "from_service_cloud_org_name": schema.StringAttribute{Optional: true, MarkdownDescription: "Org составной услуги (указывать, если resourceRealm связан с CloudDirector) Не изменяется после создания."}, + "from_service_cloud_vdc_name": schema.StringAttribute{Optional: true, MarkdownDescription: "Vdc составной услуги (указывать, если resourceRealm связан с CloudDirector) Не изменяется после создания."}, + "from_service_cloud_vmware_url": schema.StringAttribute{Optional: true, MarkdownDescription: "Api-Url составной услуги (указывать, если resourceRealm связан с CloudDirector) Не изменяется после создания."}, + "from_service_namespace": schema.StringAttribute{Required: true, MarkdownDescription: "Namespace составной услуги (для отображения правил FW) Не изменяется после создания."}, + "from_service_vdc_group_name": schema.StringAttribute{Optional: true, MarkdownDescription: "Не изменяется после создания."}, + "internal_addr_access": schema.StringAttribute{Required: true, MarkdownDescription: "Внутренний адрес для связки ната. Требует dnatCreate=true или snatCreate=true"}, + "internal_port_access": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("5432"), MarkdownDescription: "Порт для связки ната с адресом. Несколько значений можно передавать через пробел. Если передать all, то dnat будет на все порты. Требует dnatCreate=true
"}, + "ip_space_name": schema.StringAttribute{Required: true, MarkdownDescription: "Тип ip Space (internet-ipv4-v1 / internet-antiddos-v1 / internet-no-antiddos-v1)"}, + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("ngcloud.ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + "service_uid": schema.StringAttribute{Required: true}, + "snat_create": schema.BoolAttribute{Optional: true, MarkdownDescription: "Необходимо создать SNAT-правило.
"}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VcexternalipResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *VcexternalipModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *VcexternalipModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan VcexternalipModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.FromServiceCloudEdgeName.IsNull() && !plan.FromServiceCloudEdgeName.IsUnknown() && !state.FromServiceCloudEdgeName.IsNull() && !state.FromServiceCloudEdgeName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.FromServiceCloudEdgeName.ValueString(), state.FromServiceCloudEdgeName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить from_service_cloud_edge_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.FromServiceCloudEdgeScope.IsNull() && !plan.FromServiceCloudEdgeScope.IsUnknown() && !state.FromServiceCloudEdgeScope.IsNull() && !state.FromServiceCloudEdgeScope.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.FromServiceCloudEdgeScope.ValueString(), state.FromServiceCloudEdgeScope.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить from_service_cloud_edge_scope", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.FromServiceCloudOrgName.IsNull() && !plan.FromServiceCloudOrgName.IsUnknown() && !state.FromServiceCloudOrgName.IsNull() && !state.FromServiceCloudOrgName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.FromServiceCloudOrgName.ValueString(), state.FromServiceCloudOrgName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить from_service_cloud_org_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.FromServiceCloudVdcName.IsNull() && !plan.FromServiceCloudVdcName.IsUnknown() && !state.FromServiceCloudVdcName.IsNull() && !state.FromServiceCloudVdcName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.FromServiceCloudVdcName.ValueString(), state.FromServiceCloudVdcName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить from_service_cloud_vdc_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.FromServiceCloudVmwareUrl.IsNull() && !plan.FromServiceCloudVmwareUrl.IsUnknown() && !state.FromServiceCloudVmwareUrl.IsNull() && !state.FromServiceCloudVmwareUrl.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.FromServiceCloudVmwareUrl.ValueString(), state.FromServiceCloudVmwareUrl.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить from_service_cloud_vmware_url", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.FromServiceNamespace.IsNull() && !plan.FromServiceNamespace.IsUnknown() && !state.FromServiceNamespace.IsNull() && !state.FromServiceNamespace.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.FromServiceNamespace.ValueString(), state.FromServiceNamespace.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить from_service_namespace", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.FromServiceVdcGroupName.IsNull() && !plan.FromServiceVdcGroupName.IsUnknown() && !state.FromServiceVdcGroupName.IsNull() && !state.FromServiceVdcGroupName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.FromServiceVdcGroupName.ValueString(), state.FromServiceVdcGroupName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить from_service_vdc_group_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.FromServiceNamespace.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "from_service_namespace is required.") + return + } + if config.FromServiceNamespace.IsUnknown() { + return + } + if config.InternalAddrAccess.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "internal_addr_access is required.") + return + } + if config.InternalAddrAccess.IsUnknown() { + return + } + if config.IpSpaceName.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "ip_space_name is required.") + return + } + if config.IpSpaceName.IsUnknown() { + return + } + if config.ServiceUid.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "service_uid is required.") + return + } + if config.ServiceUid.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 140: resources_core.FormatString(config.ServiceUid), + 141: resources_core.FormatBool(config.DnatCreate), + 142: resources_core.FormatString(config.InternalPortAccess), + 143: resources_core.FormatBool(config.SnatCreate), + 144: resources_core.FormatString(config.InternalAddrAccess), + 324: resources_core.FormatString(config.FromServiceNamespace), + 325: resources_core.FormatString(config.FromServiceCloudEdgeName), + 326: resources_core.FormatString(config.FromServiceCloudVdcName), + 327: resources_core.FormatString(config.FromServiceCloudOrgName), + 328: resources_core.FormatString(config.FromServiceCloudVmwareUrl), + 336: resources_core.FormatString(config.IpSpaceName), + 488: resources_core.FormatString(config.ResourceRealm), + 624: resources_core.FormatString(config.FromServiceCloudEdgeScope), + 626: resources_core.FormatString(config.FromServiceVdcGroupName), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 25, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *VcexternalipResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data VcexternalipModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.FromServiceNamespace.IsNull() || data.FromServiceNamespace.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "from_service_namespace is required.") + return + } + if data.InternalAddrAccess.IsNull() || data.InternalAddrAccess.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "internal_addr_access is required.") + return + } + if data.IpSpaceName.IsNull() || data.IpSpaceName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "ip_space_name is required.") + return + } + if data.ServiceUid.IsNull() || data.ServiceUid.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "service_uid is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 25, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 140: resources_core.FormatString(data.ServiceUid), + 141: resources_core.FormatBool(data.DnatCreate), + 142: resources_core.FormatString(data.InternalPortAccess), + 143: resources_core.FormatBool(data.SnatCreate), + 144: resources_core.FormatString(data.InternalAddrAccess), + 324: resources_core.FormatString(data.FromServiceNamespace), + 325: resources_core.FormatString(data.FromServiceCloudEdgeName), + 326: resources_core.FormatString(data.FromServiceCloudVdcName), + 327: resources_core.FormatString(data.FromServiceCloudOrgName), + 328: resources_core.FormatString(data.FromServiceCloudVmwareUrl), + 336: resources_core.FormatString(data.IpSpaceName), + 488: resources_core.FormatString(data.ResourceRealm), + 624: resources_core.FormatString(data.FromServiceCloudEdgeScope), + 626: resources_core.FormatString(data.FromServiceVdcGroupName), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 25, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 25, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "dnatCreate", Field: "DnatCreate", Type: "bool"}, + {Code: "fromServiceCloudEdgeName", Field: "FromServiceCloudEdgeName", Type: "string"}, + {Code: "fromServiceCloudEdgeScope", Field: "FromServiceCloudEdgeScope", Type: "string"}, + {Code: "fromServiceCloudOrgName", Field: "FromServiceCloudOrgName", Type: "string"}, + {Code: "fromServiceCloudVdcName", Field: "FromServiceCloudVdcName", Type: "string"}, + {Code: "fromServiceCloudVmwareUrl", Field: "FromServiceCloudVmwareUrl", Type: "string"}, + {Code: "fromServiceNamespace", Field: "FromServiceNamespace", Type: "string"}, + {Code: "fromServiceVdcGroupName", Field: "FromServiceVdcGroupName", Type: "string"}, + {Code: "internalAddrAccess", Field: "InternalAddrAccess", Type: "string"}, + {Code: "internalPortAccess", Field: "InternalPortAccess", Type: "string"}, + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "serviceUid", Field: "ServiceUid", Type: "string"}, + {Code: "snatCreate", Field: "SnatCreate", Type: "bool"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcexternalipResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VcexternalipModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 25, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "dnatCreate", Field: "DnatCreate", Type: "bool"}, + {Code: "fromServiceCloudEdgeName", Field: "FromServiceCloudEdgeName", Type: "string"}, + {Code: "fromServiceCloudEdgeScope", Field: "FromServiceCloudEdgeScope", Type: "string"}, + {Code: "fromServiceCloudOrgName", Field: "FromServiceCloudOrgName", Type: "string"}, + {Code: "fromServiceCloudVdcName", Field: "FromServiceCloudVdcName", Type: "string"}, + {Code: "fromServiceCloudVmwareUrl", Field: "FromServiceCloudVmwareUrl", Type: "string"}, + {Code: "fromServiceNamespace", Field: "FromServiceNamespace", Type: "string"}, + {Code: "fromServiceVdcGroupName", Field: "FromServiceVdcGroupName", Type: "string"}, + {Code: "internalAddrAccess", Field: "InternalAddrAccess", Type: "string"}, + {Code: "internalPortAccess", Field: "InternalPortAccess", Type: "string"}, + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "serviceUid", Field: "ServiceUid", Type: "string"}, + {Code: "snatCreate", Field: "SnatCreate", Type: "bool"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *VcexternalipResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VcexternalipModel + var state VcexternalipModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.ServiceUid.IsNull() != state.ServiceUid.IsNull() || plan.ServiceUid.IsUnknown() != state.ServiceUid.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ServiceUid.IsNull() && !plan.ServiceUid.IsUnknown() && !state.ServiceUid.IsNull() && !state.ServiceUid.IsUnknown() { + if !strings.EqualFold(plan.ServiceUid.ValueString(), state.ServiceUid.ValueString()) { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.IpSpaceName.IsNull() != state.IpSpaceName.IsNull() || plan.IpSpaceName.IsUnknown() != state.IpSpaceName.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.IpSpaceName.IsNull() && !plan.IpSpaceName.IsUnknown() && !state.IpSpaceName.IsNull() && !state.IpSpaceName.IsUnknown() { + if !strings.EqualFold(plan.IpSpaceName.ValueString(), state.IpSpaceName.ValueString()) { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.DnatCreate.IsNull() != state.DnatCreate.IsNull() || plan.DnatCreate.IsUnknown() != state.DnatCreate.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.DnatCreate.IsNull() && !plan.DnatCreate.IsUnknown() && !state.DnatCreate.IsNull() && !state.DnatCreate.IsUnknown() { + if plan.DnatCreate.ValueBool() != state.DnatCreate.ValueBool() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.SnatCreate.IsNull() != state.SnatCreate.IsNull() || plan.SnatCreate.IsUnknown() != state.SnatCreate.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.SnatCreate.IsNull() && !plan.SnatCreate.IsUnknown() && !state.SnatCreate.IsNull() && !state.SnatCreate.IsUnknown() { + if plan.SnatCreate.ValueBool() != state.SnatCreate.ValueBool() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.InternalPortAccess.IsNull() != state.InternalPortAccess.IsNull() || plan.InternalPortAccess.IsUnknown() != state.InternalPortAccess.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.InternalPortAccess.IsNull() && !plan.InternalPortAccess.IsUnknown() && !state.InternalPortAccess.IsNull() && !state.InternalPortAccess.IsUnknown() { + if !strings.EqualFold(plan.InternalPortAccess.ValueString(), state.InternalPortAccess.ValueString()) { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.InternalAddrAccess.IsNull() != state.InternalAddrAccess.IsNull() || plan.InternalAddrAccess.IsUnknown() != state.InternalAddrAccess.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.InternalAddrAccess.IsNull() && !plan.InternalAddrAccess.IsUnknown() && !state.InternalAddrAccess.IsNull() && !state.InternalAddrAccess.IsUnknown() { + if !strings.EqualFold(plan.InternalAddrAccess.ValueString(), state.InternalAddrAccess.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 627: resources_core.FormatString(plan.ServiceUid), + 628: resources_core.FormatString(plan.IpSpaceName), + 629: resources_core.FormatBool(plan.DnatCreate), + 630: resources_core.FormatBool(plan.SnatCreate), + 631: resources_core.FormatString(plan.InternalPortAccess), + 632: resources_core.FormatString(plan.InternalAddrAccess), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 25, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "dnatCreate", Field: "DnatCreate", Type: "bool"}, + {Code: "fromServiceCloudEdgeName", Field: "FromServiceCloudEdgeName", Type: "string"}, + {Code: "fromServiceCloudEdgeScope", Field: "FromServiceCloudEdgeScope", Type: "string"}, + {Code: "fromServiceCloudOrgName", Field: "FromServiceCloudOrgName", Type: "string"}, + {Code: "fromServiceCloudVdcName", Field: "FromServiceCloudVdcName", Type: "string"}, + {Code: "fromServiceCloudVmwareUrl", Field: "FromServiceCloudVmwareUrl", Type: "string"}, + {Code: "fromServiceNamespace", Field: "FromServiceNamespace", Type: "string"}, + {Code: "fromServiceVdcGroupName", Field: "FromServiceVdcGroupName", Type: "string"}, + {Code: "internalAddrAccess", Field: "InternalAddrAccess", Type: "string"}, + {Code: "internalPortAccess", Field: "InternalPortAccess", Type: "string"}, + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "serviceUid", Field: "ServiceUid", Type: "string"}, + {Code: "snatCreate", Field: "SnatCreate", Type: "bool"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcexternalipResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VcexternalipModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *VcexternalipResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *VcexternalipResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/26_vapp_resource.go b/generated/prod/go/26_vapp_resource.go new file mode 100644 index 0000000..158be46 --- /dev/null +++ b/generated/prod/go/26_vapp_resource.go @@ -0,0 +1,571 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vapp +// Service ID: 26 + +var _ resource.Resource = &VappResource{} +var _ resource.ResourceWithModifyPlan = &VappResource{} +var _ resource.ResourceWithImportState = &VappResource{} + +type VappResource struct { + client *core.UniversalClient +} + +type VappModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + NsxtUid types.String `tfsdk:"nsxt_uid"` + VappName types.String `tfsdk:"vapp_name"` + VdcUid types.String `tfsdk:"vdc_uid"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewVappResource() resource.Resource { + return &VappResource{} +} + +func (r *VappResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vapp" +} + +func (r *VappResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "nsxt_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UUID Услуги `Сетевой шлюз периметра (Edge)` Не изменяется после создания."}, + "vapp_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("3ecobacg"), MarkdownDescription: "Уникальное в рамках организации Не изменяется после создания."}, + "vdc_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UUID Услуги `Виртуальный датацентр (vDC)` Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VappResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *VappModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *VappModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan VappModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan. + // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов. + // Resolve (displayName → UUID или uppercase → lowercase) нужен только для + // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка + // "Provider produced invalid plan: planned value does not match config value". + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.NsxtUid.IsNull() && !plan.NsxtUid.IsUnknown() && !state.NsxtUid.IsNull() && !state.NsxtUid.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.NsxtUid.ValueString(), state.NsxtUid.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить nsxt_uid", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.VappName.IsNull() && !plan.VappName.IsUnknown() && !state.VappName.IsNull() && !state.VappName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VappName.ValueString(), state.VappName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить vapp_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.VdcUid.IsNull() && !plan.VdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VdcUid.ValueString(), state.VdcUid.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить vdc_uid", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.NsxtUid.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "nsxt_uid is required.") + return + } + if config.NsxtUid.IsUnknown() { + return + } + if config.VdcUid.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "vdc_uid is required.") + return + } + if config.VdcUid.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + if !config.NsxtUid.IsNull() && !config.NsxtUid.IsUnknown() { + resolvedNsxtUid, err := r.client.ResolveRefSvcParamValue(ctx, 22, config.NsxtUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve nsxt_uid", err.Error()) + } else if resolvedNsxtUid != "" && resolvedNsxtUid != config.NsxtUid.ValueString() { + config.NsxtUid = types.StringValue(resolvedNsxtUid) + } + } + if !config.VdcUid.IsNull() && !config.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, config.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error()) + } else if resolvedVdcUid != "" && resolvedVdcUid != config.VdcUid.ValueString() { + config.VdcUid = types.StringValue(resolvedVdcUid) + } + } + params := map[int]string{ + 190: resources_core.FormatString(config.NsxtUid), + 191: resources_core.FormatString(config.VappName), + 623: resources_core.FormatString(config.VdcUid), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 26, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *VappResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data VappModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.NsxtUid.IsNull() || data.NsxtUid.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "nsxt_uid is required.") + return + } + if data.VdcUid.IsNull() || data.VdcUid.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "vdc_uid is required.") + return + } + // ══════════════════════════════════════════════════════════════════════════ + // ПРАВИЛО TERRAFORM (официальная документация): + // «If an attribute value is configured, it is NEVER valid to change that + // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что + // написал пользователь). Менять plan запрещено на уровне фреймворка. + // + // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре. + // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный). + // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит: + // «Provider produced inconsistent result after apply». + // + // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот. + // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как + // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API). + // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный + // регистр в state через strings.EqualFold (сравниваем без учёта регистра). + // ══════════════════════════════════════════════════════════════════════════ + originalNsxtUid := data.NsxtUid + // ══════════════════════════════════════════════════════════════════════════ + // ПРАВИЛО TERRAFORM (официальная документация): + // «If an attribute value is configured, it is NEVER valid to change that + // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что + // написал пользователь). Менять plan запрещено на уровне фреймворка. + // + // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре. + // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный). + // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит: + // «Provider produced inconsistent result after apply». + // + // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот. + // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как + // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API). + // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный + // регистр в state через strings.EqualFold (сравниваем без учёта регистра). + // ══════════════════════════════════════════════════════════════════════════ + originalVdcUid := data.VdcUid + if !data.NsxtUid.IsNull() && !data.NsxtUid.IsUnknown() { + resolvedNsxtUid, err := r.client.ResolveRefSvcParamValue(ctx, 22, data.NsxtUid.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if resolvedNsxtUid != "" && resolvedNsxtUid != data.NsxtUid.ValueString() { + data.NsxtUid = types.StringValue(resolvedNsxtUid) + } + } + if !data.VdcUid.IsNull() && !data.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, data.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if resolvedVdcUid != "" && resolvedVdcUid != data.VdcUid.ValueString() { + data.VdcUid = types.StringValue(resolvedVdcUid) + } + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 26, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 190: resources_core.FormatString(data.NsxtUid), + 191: resources_core.FormatString(data.VappName), + 623: resources_core.FormatString(data.VdcUid), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 26, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 26, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "nsxtUid", Field: "NsxtUid", Type: "string"}, + {Code: "vappName", Field: "VappName", Type: "string"}, + {Code: "vdcUid", Field: "VdcUid", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.NsxtUid.IsNull() && !state.NsxtUid.IsUnknown() { + resolvedNsxtUid, err := r.client.ResolveRefSvcParamValue(ctx, 22, state.NsxtUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve nsxt_uid", err.Error()) + } else if resolvedNsxtUid != "" && resolvedNsxtUid != state.NsxtUid.ValueString() { + state.NsxtUid = types.StringValue(resolvedNsxtUid) + } + } + if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, state.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error()) + } else if resolvedVdcUid != "" && resolvedVdcUid != state.VdcUid.ValueString() { + state.VdcUid = types.StringValue(resolvedVdcUid) + } + } + // Restore user-provided casing в state (Create). + // EqualFold = «совпадают ли значения без учёта регистра?» + // Если да — значит API вернул «туже» строку, только в другом регистре. + // Заменяем state на original (то что было в plan/config пользователя). + // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint. + if !originalNsxtUid.IsNull() && !originalNsxtUid.IsUnknown() && !state.NsxtUid.IsNull() && !state.NsxtUid.IsUnknown() { + if strings.EqualFold(state.NsxtUid.ValueString(), originalNsxtUid.ValueString()) { + state.NsxtUid = originalNsxtUid + } + } + if !originalVdcUid.IsNull() && !originalVdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + if strings.EqualFold(state.VdcUid.ValueString(), originalVdcUid.ValueString()) { + state.VdcUid = originalVdcUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VappResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VappModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 26, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "nsxtUid", Field: "NsxtUid", Type: "string"}, + {Code: "vappName", Field: "VappName", Type: "string"}, + {Code: "vdcUid", Field: "VdcUid", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !newState.NsxtUid.IsNull() && !newState.NsxtUid.IsUnknown() { + resolvedNsxtUid, err := r.client.ResolveRefSvcParamValue(ctx, 22, newState.NsxtUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve nsxt_uid", err.Error()) + } else if resolvedNsxtUid != "" && resolvedNsxtUid != newState.NsxtUid.ValueString() { + newState.NsxtUid = types.StringValue(resolvedNsxtUid) + } + } + if !newState.VdcUid.IsNull() && !newState.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, newState.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error()) + } else if resolvedVdcUid != "" && resolvedVdcUid != newState.VdcUid.ValueString() { + newState.VdcUid = types.StringValue(resolvedVdcUid) + } + } + // Restore user-provided casing в state (Read). + // При чтении у нас нет plan — но предыдущий state уже хранит значение + // в регистре пользователя (после первого Create оно было восстановлено). + // Берём prior state (переменная state) как эталон регистра. + // Если API вернул то же UUID только строчными буквами — восстанавливаем. + if !state.NsxtUid.IsNull() && !state.NsxtUid.IsUnknown() && !newState.NsxtUid.IsNull() && !newState.NsxtUid.IsUnknown() { + if strings.EqualFold(newState.NsxtUid.ValueString(), state.NsxtUid.ValueString()) { + newState.NsxtUid = state.NsxtUid + } + } + if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() && !newState.VdcUid.IsNull() && !newState.VdcUid.IsUnknown() { + if strings.EqualFold(newState.VdcUid.ValueString(), state.VdcUid.ValueString()) { + newState.VdcUid = state.VdcUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *VappResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VappModel + var state VappModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 26, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "nsxtUid", Field: "NsxtUid", Type: "string"}, + {Code: "vappName", Field: "VappName", Type: "string"}, + {Code: "vdcUid", Field: "VdcUid", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.NsxtUid.IsNull() && !state.NsxtUid.IsUnknown() { + resolvedNsxtUid, err := r.client.ResolveRefSvcParamValue(ctx, 22, state.NsxtUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve nsxt_uid", err.Error()) + } else if resolvedNsxtUid != "" && resolvedNsxtUid != state.NsxtUid.ValueString() { + state.NsxtUid = types.StringValue(resolvedNsxtUid) + } + } + if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, state.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error()) + } else if resolvedVdcUid != "" && resolvedVdcUid != state.VdcUid.ValueString() { + state.VdcUid = types.StringValue(resolvedVdcUid) + } + } + // Restore user-provided casing в state (Update). + // После API-вызова modify state содержит значения в lower-case от API. + // Plan == config == то что написал пользователь (регистр неизменён). + // EqualFold: если UUID совпадает без учёта регистра — берём из plan. + if !plan.NsxtUid.IsNull() && !plan.NsxtUid.IsUnknown() && !state.NsxtUid.IsNull() && !state.NsxtUid.IsUnknown() { + if strings.EqualFold(state.NsxtUid.ValueString(), plan.NsxtUid.ValueString()) { + state.NsxtUid = plan.NsxtUid + } + } + if !plan.VdcUid.IsNull() && !plan.VdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + if strings.EqualFold(state.VdcUid.ValueString(), plan.VdcUid.ValueString()) { + state.VdcUid = plan.VdcUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VappResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VappModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *VappResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *VappResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/27_vc_vm_v2_resource.go b/generated/prod/go/27_vc_vm_v2_resource.go new file mode 100644 index 0000000..afd6fec --- /dev/null +++ b/generated/prod/go/27_vc_vm_v2_resource.go @@ -0,0 +1,350 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vc_vm_v2 +// Service ID: 27 + +var _ resource.Resource = &VcVmV2Resource{} +var _ resource.ResourceWithModifyPlan = &VcVmV2Resource{} +var _ resource.ResourceWithImportState = &VcVmV2Resource{} + +type VcVmV2Resource struct { + client *core.UniversalClient +} + +type VcVmV2Model struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + ServerListJson types.String `tfsdk:"server_list_json"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewVcVmV2Resource() resource.Resource { + return &VcVmV2Resource{} +} + +func (r *VcVmV2Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vc_vm_v2" +} + +func (r *VcVmV2Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "server_list_json": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Необходимо изменить vappUid и ipSpaceName
Пример конфигурации взять по ссылке ниже

Объект серверов вида [{}] (Map(Object))
Более подробнее смотреть в README.md (https://gitea.mgmt.nubes.ru/cloudServices/vcVmV2)", PlanModifiers: []planmodifier.String{resources_core.JsonNormalize()}}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VcVmV2Resource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *VcVmV2Model + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *VcVmV2Model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan VcVmV2Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 301: resources_core.FormatString(config.ServerListJson), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 27, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *VcVmV2Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data VcVmV2Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 27, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 301: resources_core.FormatString(data.ServerListJson), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 27, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 27, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "serverListJson", Field: "ServerListJson", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcVmV2Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VcVmV2Model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 27, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "serverListJson", Field: "ServerListJson", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *VcVmV2Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VcVmV2Model + var state VcVmV2Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.ServerListJson.IsNull() != state.ServerListJson.IsNull() || plan.ServerListJson.IsUnknown() != state.ServerListJson.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ServerListJson.IsNull() && !plan.ServerListJson.IsUnknown() && !state.ServerListJson.IsNull() && !state.ServerListJson.IsUnknown() { + if !strings.EqualFold(plan.ServerListJson.ValueString(), state.ServerListJson.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 302: resources_core.FormatString(plan.ServerListJson), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 27, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "serverListJson", Field: "ServerListJson", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcVmV2Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VcVmV2Model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *VcVmV2Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *VcVmV2Resource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/28_vc_vm_v3_resource.go b/generated/prod/go/28_vc_vm_v3_resource.go new file mode 100644 index 0000000..89e6d1e --- /dev/null +++ b/generated/prod/go/28_vc_vm_v3_resource.go @@ -0,0 +1,697 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vc_vm_v3 +// Service ID: 28 + +var _ resource.Resource = &VcVmV3Resource{} +var _ resource.ResourceWithModifyPlan = &VcVmV3Resource{} +var _ resource.ResourceWithImportState = &VcVmV3Resource{} + +type VcVmV3Resource struct { + client *core.UniversalClient +} + +type VcVmV3Model struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + // --- Redeploy support (ARCHITECTURE.md) --- + // git_revision: при изменении вызывает redeploy вместо modify. + // Опциональное поле — если не задано, modify работает как обычно. + GitRevision types.String `tfsdk:"git_revision"` + AccessIpList types.String `tfsdk:"access_ip_list"` + AccessPortList types.String `tfsdk:"access_port_list"` + CloudInit types.String `tfsdk:"cloud_init"` + ImageVm types.String `tfsdk:"image_vm"` + IpSpaceName types.String `tfsdk:"ip_space_name"` + UserLogin types.String `tfsdk:"user_login"` + UserPublicKey types.String `tfsdk:"user_public_key"` + VappUid types.String `tfsdk:"vapp_uid"` + VmCpu types.Int64 `tfsdk:"vm_cpu"` + VmDisk types.Int64 `tfsdk:"vm_disk"` + VmName types.String `tfsdk:"vm_name"` + VmRam types.Int64 `tfsdk:"vm_ram"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewVcVmV3Resource() resource.Resource { + return &VcVmV3Resource{} +} + +func (r *VcVmV3Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vc_vm_v3" +} + +func (r *VcVmV3Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_ip_list": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "Формат массива
Каким адресам будет предоставлен доступ до вм

Требует выделенного внешнего IP
Если ничего не передано, то выставлено ["0.0.0.0/0"]", PlanModifiers: []planmodifier.String{resources_core.JsonNormalize()}}, + "access_port_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[\"22\"]"), MarkdownDescription: "По каким портам открыть доступ к ВМ.
Порты 22, 10050 проставляются автоматически
Пока доступно:
22 - SSH,
80 - HTTP,
443 - HTTPS,
10050 - ZABBIX"}, + "cloud_init": schema.StringAttribute{Optional: true, MarkdownDescription: "Можно передать создание дополнительных пользователей.
На текущий момент не реализовано выполнения в cloud-init, установки пакетов, выполнения команд и записи файлов. Не изменяется после создания."}, + "image_vm": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания."}, + "ip_space_name": schema.StringAttribute{Required: true}, + "user_login": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("myuser"), MarkdownDescription: "Учётка SSH Не изменяется после создания."}, + "user_public_key": schema.StringAttribute{Required: true, MarkdownDescription: "Публичная часть SSH-Ключа для пользователя Не изменяется после создания."}, + "vapp_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UI display_name for service_id=26 (mapped to UUID in core) Не изменяется после создания."}, + "vm_cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "шт"}, + "vm_disk": schema.Int64Attribute{Optional: true, MarkdownDescription: "Основной диск у vm зависит от образа (см. документацию)
Если необходимо дополнительное место для vm, введите колличество Gb в эту форму"}, + "vm_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("web01"), MarkdownDescription: "Не изменяется после создания."}, + "vm_ram": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Gb"}, + // --- Redeploy support (ARCHITECTURE.md) --- + // При изменении git_revision вызывается redeploy вместо modify. + // Если не задано — modify работает без изменений. + "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VcVmV3Resource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *VcVmV3Model + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *VcVmV3Model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan VcVmV3Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan. + // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов. + // Resolve (displayName → UUID или uppercase → lowercase) нужен только для + // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка + // "Provider produced invalid plan: planned value does not match config value". + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.CloudInit.IsNull() && !plan.CloudInit.IsUnknown() && !state.CloudInit.IsNull() && !state.CloudInit.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.CloudInit.ValueString(), state.CloudInit.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить cloud_init", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ImageVm.IsNull() && !plan.ImageVm.IsUnknown() && !state.ImageVm.IsNull() && !state.ImageVm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ImageVm.ValueString(), state.ImageVm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить image_vm", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.UserLogin.IsNull() && !plan.UserLogin.IsUnknown() && !state.UserLogin.IsNull() && !state.UserLogin.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.UserLogin.ValueString(), state.UserLogin.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить user_login", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.UserPublicKey.IsNull() && !plan.UserPublicKey.IsUnknown() && !state.UserPublicKey.IsNull() && !state.UserPublicKey.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.UserPublicKey.ValueString(), state.UserPublicKey.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить user_public_key", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.VappUid.IsNull() && !plan.VappUid.IsUnknown() && !state.VappUid.IsNull() && !state.VappUid.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VappUid.ValueString(), state.VappUid.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить vapp_uid", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.VmName.IsNull() && !plan.VmName.IsUnknown() && !state.VmName.IsNull() && !state.VmName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VmName.ValueString(), state.VmName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить vm_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.ImageVm.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "image_vm is required.") + return + } + if config.ImageVm.IsUnknown() { + return + } + if config.IpSpaceName.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "ip_space_name is required.") + return + } + if config.IpSpaceName.IsUnknown() { + return + } + if config.UserPublicKey.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "user_public_key is required.") + return + } + if config.UserPublicKey.IsUnknown() { + return + } + if config.VappUid.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "vapp_uid is required.") + return + } + if config.VappUid.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + if !config.VappUid.IsNull() && !config.VappUid.IsUnknown() { + resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, config.VappUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error()) + } else if resolvedVappUid != "" && resolvedVappUid != config.VappUid.ValueString() { + config.VappUid = types.StringValue(resolvedVappUid) + } + } + params := map[int]string{ + 407: resources_core.FormatString(config.VappUid), + 408: resources_core.FormatString(config.VmName), + 409: resources_core.FormatInt64(config.VmCpu), + 410: resources_core.FormatInt64(config.VmRam), + 411: resources_core.FormatInt64(config.VmDisk), + 412: resources_core.FormatString(config.IpSpaceName), + 413: resources_core.FormatString(config.AccessIpList), + 414: resources_core.FormatString(config.ImageVm), + 415: resources_core.FormatString(config.CloudInit), + 416: resources_core.FormatString(config.UserLogin), + 417: resources_core.FormatString(config.UserPublicKey), + 448: resources_core.FormatString(config.AccessPortList), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 28, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *VcVmV3Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data VcVmV3Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.ImageVm.IsNull() || data.ImageVm.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "image_vm is required.") + return + } + if data.IpSpaceName.IsNull() || data.IpSpaceName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "ip_space_name is required.") + return + } + if data.UserPublicKey.IsNull() || data.UserPublicKey.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "user_public_key is required.") + return + } + if data.VappUid.IsNull() || data.VappUid.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "vapp_uid is required.") + return + } + // ══════════════════════════════════════════════════════════════════════════ + // ПРАВИЛО TERRAFORM (официальная документация): + // «If an attribute value is configured, it is NEVER valid to change that + // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что + // написал пользователь). Менять plan запрещено на уровне фреймворка. + // + // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре. + // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный). + // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит: + // «Provider produced inconsistent result after apply». + // + // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот. + // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как + // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API). + // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный + // регистр в state через strings.EqualFold (сравниваем без учёта регистра). + // ══════════════════════════════════════════════════════════════════════════ + originalVappUid := data.VappUid + if !data.VappUid.IsNull() && !data.VappUid.IsUnknown() { + resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, data.VappUid.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if resolvedVappUid != "" && resolvedVappUid != data.VappUid.ValueString() { + data.VappUid = types.StringValue(resolvedVappUid) + } + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 28, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 407: resources_core.FormatString(data.VappUid), + 408: resources_core.FormatString(data.VmName), + 409: resources_core.FormatInt64(data.VmCpu), + 410: resources_core.FormatInt64(data.VmRam), + 411: resources_core.FormatInt64(data.VmDisk), + 412: resources_core.FormatString(data.IpSpaceName), + 413: resources_core.FormatString(data.AccessIpList), + 414: resources_core.FormatString(data.ImageVm), + 415: resources_core.FormatString(data.CloudInit), + 416: resources_core.FormatString(data.UserLogin), + 417: resources_core.FormatString(data.UserPublicKey), + 448: resources_core.FormatString(data.AccessPortList), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 28, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 28, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessIpList", Field: "AccessIpList", Type: "string"}, + {Code: "accessPortList", Field: "AccessPortList", Type: "string"}, + {Code: "cloudInit", Field: "CloudInit", Type: "string"}, + {Code: "imageVm", Field: "ImageVm", Type: "string"}, + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "userLogin", Field: "UserLogin", Type: "string"}, + {Code: "userPublicKey", Field: "UserPublicKey", Type: "string"}, + {Code: "vappUid", Field: "VappUid", Type: "string"}, + {Code: "vmCpu", Field: "VmCpu", Type: "int64"}, + {Code: "vmDisk", Field: "VmDisk", Type: "int64"}, + {Code: "vmName", Field: "VmName", Type: "string"}, + {Code: "vmRam", Field: "VmRam", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.VappUid.IsNull() && !state.VappUid.IsUnknown() { + resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, state.VappUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error()) + } else if resolvedVappUid != "" && resolvedVappUid != state.VappUid.ValueString() { + state.VappUid = types.StringValue(resolvedVappUid) + } + } + // Restore user-provided casing в state (Create). + // EqualFold = «совпадают ли значения без учёта регистра?» + // Если да — значит API вернул «туже» строку, только в другом регистре. + // Заменяем state на original (то что было в plan/config пользователя). + // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint. + if !originalVappUid.IsNull() && !originalVappUid.IsUnknown() && !state.VappUid.IsNull() && !state.VappUid.IsUnknown() { + if strings.EqualFold(state.VappUid.ValueString(), originalVappUid.ValueString()) { + state.VappUid = originalVappUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcVmV3Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VcVmV3Model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 28, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessIpList", Field: "AccessIpList", Type: "string"}, + {Code: "accessPortList", Field: "AccessPortList", Type: "string"}, + {Code: "cloudInit", Field: "CloudInit", Type: "string"}, + {Code: "imageVm", Field: "ImageVm", Type: "string"}, + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "userLogin", Field: "UserLogin", Type: "string"}, + {Code: "userPublicKey", Field: "UserPublicKey", Type: "string"}, + {Code: "vappUid", Field: "VappUid", Type: "string"}, + {Code: "vmCpu", Field: "VmCpu", Type: "int64"}, + {Code: "vmDisk", Field: "VmDisk", Type: "int64"}, + {Code: "vmName", Field: "VmName", Type: "string"}, + {Code: "vmRam", Field: "VmRam", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !newState.VappUid.IsNull() && !newState.VappUid.IsUnknown() { + resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, newState.VappUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error()) + } else if resolvedVappUid != "" && resolvedVappUid != newState.VappUid.ValueString() { + newState.VappUid = types.StringValue(resolvedVappUid) + } + } + // Restore user-provided casing в state (Read). + // При чтении у нас нет plan — но предыдущий state уже хранит значение + // в регистре пользователя (после первого Create оно было восстановлено). + // Берём prior state (переменная state) как эталон регистра. + // Если API вернул то же UUID только строчными буквами — восстанавливаем. + if !state.VappUid.IsNull() && !state.VappUid.IsUnknown() && !newState.VappUid.IsNull() && !newState.VappUid.IsUnknown() { + if strings.EqualFold(newState.VappUid.ValueString(), state.VappUid.ValueString()) { + newState.VappUid = state.VappUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *VcVmV3Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VcVmV3Model + var state VcVmV3Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.VmCpu.IsNull() != state.VmCpu.IsNull() || plan.VmCpu.IsUnknown() != state.VmCpu.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.VmCpu.IsNull() && !plan.VmCpu.IsUnknown() && !state.VmCpu.IsNull() && !state.VmCpu.IsUnknown() { + if plan.VmCpu.ValueInt64() != state.VmCpu.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.VmRam.IsNull() != state.VmRam.IsNull() || plan.VmRam.IsUnknown() != state.VmRam.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.VmRam.IsNull() && !plan.VmRam.IsUnknown() && !state.VmRam.IsNull() && !state.VmRam.IsUnknown() { + if plan.VmRam.ValueInt64() != state.VmRam.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.VmDisk.IsNull() != state.VmDisk.IsNull() || plan.VmDisk.IsUnknown() != state.VmDisk.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.VmDisk.IsNull() && !plan.VmDisk.IsUnknown() && !state.VmDisk.IsNull() && !state.VmDisk.IsUnknown() { + if plan.VmDisk.ValueInt64() != state.VmDisk.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.IpSpaceName.IsNull() != state.IpSpaceName.IsNull() || plan.IpSpaceName.IsUnknown() != state.IpSpaceName.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.IpSpaceName.IsNull() && !plan.IpSpaceName.IsUnknown() && !state.IpSpaceName.IsNull() && !state.IpSpaceName.IsUnknown() { + if !strings.EqualFold(plan.IpSpaceName.ValueString(), state.IpSpaceName.ValueString()) { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.AccessIpList.IsNull() != state.AccessIpList.IsNull() || plan.AccessIpList.IsUnknown() != state.AccessIpList.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.AccessIpList.IsNull() && !plan.AccessIpList.IsUnknown() && !state.AccessIpList.IsNull() && !state.AccessIpList.IsUnknown() { + if !strings.EqualFold(plan.AccessIpList.ValueString(), state.AccessIpList.ValueString()) { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.AccessPortList.IsNull() != state.AccessPortList.IsNull() || plan.AccessPortList.IsUnknown() != state.AccessPortList.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.AccessPortList.IsNull() && !plan.AccessPortList.IsUnknown() && !state.AccessPortList.IsNull() && !state.AccessPortList.IsUnknown() { + if !strings.EqualFold(plan.AccessPortList.ValueString(), state.AccessPortList.ValueString()) { + hasServiceParamChanges = true + } + } + } + // --- Redeploy support (ARCHITECTURE.md) --- + // Проверяем изменился ли git_revision. + // Если да — вызываем redeploy вместо modify. + // git_revision = "" (не задано) → обычный modify. + redeployRequested := false + if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() { + if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() { + redeployRequested = true + } + } + + if !hasServiceParamChanges && !redeployRequested { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + // --- Redeploy support (ARCHITECTURE.md) --- + if redeployRequested { + // Вызываем redeploy — пустой params (операция не требует CFS-параметров) + if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + } else { + params := map[int]string{ + 493: resources_core.FormatInt64(plan.VmCpu), + 494: resources_core.FormatInt64(plan.VmRam), + 495: resources_core.FormatInt64(plan.VmDisk), + 496: resources_core.FormatString(plan.IpSpaceName), + 497: resources_core.FormatString(plan.AccessIpList), + 498: resources_core.FormatString(plan.AccessPortList), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 28, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessIpList", Field: "AccessIpList", Type: "string"}, + {Code: "accessPortList", Field: "AccessPortList", Type: "string"}, + {Code: "cloudInit", Field: "CloudInit", Type: "string"}, + {Code: "imageVm", Field: "ImageVm", Type: "string"}, + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "userLogin", Field: "UserLogin", Type: "string"}, + {Code: "userPublicKey", Field: "UserPublicKey", Type: "string"}, + {Code: "vappUid", Field: "VappUid", Type: "string"}, + {Code: "vmCpu", Field: "VmCpu", Type: "int64"}, + {Code: "vmDisk", Field: "VmDisk", Type: "int64"}, + {Code: "vmName", Field: "VmName", Type: "string"}, + {Code: "vmRam", Field: "VmRam", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.VappUid.IsNull() && !state.VappUid.IsUnknown() { + resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, state.VappUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error()) + } else if resolvedVappUid != "" && resolvedVappUid != state.VappUid.ValueString() { + state.VappUid = types.StringValue(resolvedVappUid) + } + } + // Restore user-provided casing в state (Update). + // После API-вызова modify state содержит значения в lower-case от API. + // Plan == config == то что написал пользователь (регистр неизменён). + // EqualFold: если UUID совпадает без учёта регистра — берём из plan. + if !plan.VappUid.IsNull() && !plan.VappUid.IsUnknown() && !state.VappUid.IsNull() && !state.VappUid.IsUnknown() { + if strings.EqualFold(state.VappUid.ValueString(), plan.VappUid.ValueString()) { + state.VappUid = plan.VappUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcVmV3Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VcVmV3Model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *VcVmV3Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *VcVmV3Resource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/29_vc_vdc_group_resource.go b/generated/prod/go/29_vc_vdc_group_resource.go new file mode 100644 index 0000000..f5a5213 --- /dev/null +++ b/generated/prod/go/29_vc_vdc_group_resource.go @@ -0,0 +1,449 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vc_vdc_group +// Service ID: 29 + +var _ resource.Resource = &VcVdcGroupResource{} +var _ resource.ResourceWithModifyPlan = &VcVdcGroupResource{} +var _ resource.ResourceWithImportState = &VcVdcGroupResource{} + +type VcVdcGroupResource struct { + client *core.UniversalClient +} + +type VcVdcGroupModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + VdcUid types.String `tfsdk:"vdc_uid"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewVcVdcGroupResource() resource.Resource { + return &VcVdcGroupResource{} +} + +func (r *VcVdcGroupResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vc_vdc_group" +} + +func (r *VcVdcGroupResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "vdc_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UI display_name for service_id=21 (mapped to UUID in core) Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VcVdcGroupResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *VcVdcGroupModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *VcVdcGroupModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan VcVdcGroupModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan. + // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов. + // Resolve (displayName → UUID или uppercase → lowercase) нужен только для + // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка + // "Provider produced invalid plan: planned value does not match config value". + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.VdcUid.IsNull() && !plan.VdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VdcUid.ValueString(), state.VdcUid.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить vdc_uid", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.VdcUid.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "vdc_uid is required.") + return + } + if config.VdcUid.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + if !config.VdcUid.IsNull() && !config.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, config.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error()) + } else if resolvedVdcUid != "" && resolvedVdcUid != config.VdcUid.ValueString() { + config.VdcUid = types.StringValue(resolvedVdcUid) + } + } + params := map[int]string{ + 618: resources_core.FormatString(config.VdcUid), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 29, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *VcVdcGroupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data VcVdcGroupModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.VdcUid.IsNull() || data.VdcUid.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "vdc_uid is required.") + return + } + // ══════════════════════════════════════════════════════════════════════════ + // ПРАВИЛО TERRAFORM (официальная документация): + // «If an attribute value is configured, it is NEVER valid to change that + // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что + // написал пользователь). Менять plan запрещено на уровне фреймворка. + // + // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре. + // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный). + // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит: + // «Provider produced inconsistent result after apply». + // + // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот. + // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как + // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API). + // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный + // регистр в state через strings.EqualFold (сравниваем без учёта регистра). + // ══════════════════════════════════════════════════════════════════════════ + originalVdcUid := data.VdcUid + if !data.VdcUid.IsNull() && !data.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, data.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if resolvedVdcUid != "" && resolvedVdcUid != data.VdcUid.ValueString() { + data.VdcUid = types.StringValue(resolvedVdcUid) + } + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 29, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 618: resources_core.FormatString(data.VdcUid), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 29, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 29, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "vdcUid", Field: "VdcUid", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, state.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error()) + } else if resolvedVdcUid != "" && resolvedVdcUid != state.VdcUid.ValueString() { + state.VdcUid = types.StringValue(resolvedVdcUid) + } + } + // Restore user-provided casing в state (Create). + // EqualFold = «совпадают ли значения без учёта регистра?» + // Если да — значит API вернул «туже» строку, только в другом регистре. + // Заменяем state на original (то что было в plan/config пользователя). + // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint. + if !originalVdcUid.IsNull() && !originalVdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + if strings.EqualFold(state.VdcUid.ValueString(), originalVdcUid.ValueString()) { + state.VdcUid = originalVdcUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcVdcGroupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VcVdcGroupModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 29, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "vdcUid", Field: "VdcUid", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !newState.VdcUid.IsNull() && !newState.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, newState.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error()) + } else if resolvedVdcUid != "" && resolvedVdcUid != newState.VdcUid.ValueString() { + newState.VdcUid = types.StringValue(resolvedVdcUid) + } + } + // Restore user-provided casing в state (Read). + // При чтении у нас нет plan — но предыдущий state уже хранит значение + // в регистре пользователя (после первого Create оно было восстановлено). + // Берём prior state (переменная state) как эталон регистра. + // Если API вернул то же UUID только строчными буквами — восстанавливаем. + if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() && !newState.VdcUid.IsNull() && !newState.VdcUid.IsUnknown() { + if strings.EqualFold(newState.VdcUid.ValueString(), state.VdcUid.ValueString()) { + newState.VdcUid = state.VdcUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *VcVdcGroupResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VcVdcGroupModel + var state VcVdcGroupModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 29, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "vdcUid", Field: "VdcUid", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, state.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error()) + } else if resolvedVdcUid != "" && resolvedVdcUid != state.VdcUid.ValueString() { + state.VdcUid = types.StringValue(resolvedVdcUid) + } + } + // Restore user-provided casing в state (Update). + // После API-вызова modify state содержит значения в lower-case от API. + // Plan == config == то что написал пользователь (регистр неизменён). + // EqualFold: если UUID совпадает без учёта регистра — берём из plan. + if !plan.VdcUid.IsNull() && !plan.VdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + if strings.EqualFold(state.VdcUid.ValueString(), plan.VdcUid.ValueString()) { + state.VdcUid = plan.VdcUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcVdcGroupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VcVdcGroupModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *VcVdcGroupResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *VcVdcGroupResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/29_vc_vdc_group_vdc_resource.go b/generated/prod/go/29_vc_vdc_group_vdc_resource.go new file mode 100644 index 0000000..3e3e8ef --- /dev/null +++ b/generated/prod/go/29_vc_vdc_group_vdc_resource.go @@ -0,0 +1,326 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vc_vdc_group +// Subresource: vdc + +var _ resource.Resource = &VcVdcGroupVdcResource{} + +// VcVdcGroupVdcResource manages a subresource via operations. +type VcVdcGroupVdcResource struct { + client *core.UniversalClient +} + +type VcVdcGroupVdcModel struct { + ID types.String `tfsdk:"id"` + VcVdcGroupID types.String `tfsdk:"vc_vdc_group_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` +} + +func NewVcVdcGroupVdcResource() resource.Resource { + return &VcVdcGroupVdcResource{} +} + +func (r *VcVdcGroupVdcResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vc_vdc_group_vdc" +} + +func (r *VcVdcGroupVdcResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "vc_vdc_group_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VcVdcGroupVdcResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan VcVdcGroupVdcModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.VcVdcGroupID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{} + identityCodes := []string{} + listKey, idKey := resources_core.ResolveSubresourceStateKeys("vdc", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "vdc", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{}) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "vdc", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "vdc", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "vdc", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *VcVdcGroupVdcResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VcVdcGroupVdcModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcVdcGroupVdcResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VcVdcGroupVdcModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state VcVdcGroupVdcModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.VcVdcGroupID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{} + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "vdc", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *VcVdcGroupVdcResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VcVdcGroupVdcModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + return + } + + instanceUID := strings.TrimSpace(state.VcVdcGroupID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{} + identityCodes := []string{} + listKey, idKey := resources_core.ResolveSubresourceStateKeys("vdc", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{}) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *VcVdcGroupVdcResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/2_template_resource.go b/generated/prod/go/2_template_resource.go new file mode 100644 index 0000000..a7a5bbb --- /dev/null +++ b/generated/prod/go/2_template_resource.go @@ -0,0 +1,397 @@ +package resources_gen + +import ( + "context" + "fmt" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: template +// Service ID: 2 + +var _ resource.Resource = &TemplateResource{} +var _ resource.ResourceWithModifyPlan = &TemplateResource{} +var _ resource.ResourceWithImportState = &TemplateResource{} + +type TemplateResource struct { + client *core.UniversalClient +} + +// TemplateAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration. +type TemplateAccessConfigurationModel struct { + SlaveAccessList types.String `tfsdk:"slave_access_list" json:"slaveAccessList"` + Type types.String `tfsdk:"type" json:"type"` + Domain types.String `tfsdk:"domain" json:"domain"` + MasterIpSpace types.String `tfsdk:"master_ip_space" json:"masterIpSpace"` + MasterAccessList types.String `tfsdk:"master_access_list" json:"masterAccessList"` + SlaveIpSpace types.String `tfsdk:"slave_ip_space" json:"slaveIpSpace"` +} + +// TemplateClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type TemplateClusterConfigurationModel struct { + Memory types.Int64 `tfsdk:"memory" json:"memory"` + Replicas types.Int64 `tfsdk:"replicas" json:"replicas"` + Cpu types.Int64 `tfsdk:"cpu" json:"cpu"` + Disk types.Int64 `tfsdk:"disk" json:"disk"` +} + +// TemplateStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type TemplateStartupConfigurationModel struct { + ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"` +} + +type TemplateModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessConfiguration *TemplateAccessConfigurationModel `tfsdk:"access_configuration"` + ClusterConfiguration *TemplateClusterConfigurationModel `tfsdk:"cluster_configuration"` + StartupConfiguration *TemplateStartupConfigurationModel `tfsdk:"startup_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewTemplateResource() resource.Resource { + return &TemplateResource{} +} + +func (r *TemplateResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_template" +} + +func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "slave_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]")}, + "type": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("ingress")}, + "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("abracadabra-8htr2l6c")}, + "master_ip_space": schema.StringAttribute{Required: true}, + "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]")}, + "slave_ip_space": schema.StringAttribute{Required: true}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(128)}, + "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)}, + "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100)}, + "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(5)}, + }}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8s-4-ext-nubes-ru")}, + }}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *TemplateResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *TemplateModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *TemplateModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan TemplateModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 813: resources_core.BuildJSON(map[string]string{"memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64())}), + 814: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}), + 815: resources_core.BuildJSON(map[string]string{"slaveAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.SlaveAccessList.ValueString()), "type": fmt.Sprintf("\"%s\"", config.AccessConfiguration.Type.ValueString()), "domain": fmt.Sprintf("\"%s\"", config.AccessConfiguration.Domain.ValueString()), "masterIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.SlaveIpSpace.ValueString())}), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 2, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *TemplateResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data TemplateModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 2, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 813: resources_core.BuildJSON(map[string]string{"memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64())}), + 814: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}), + 815: resources_core.BuildJSON(map[string]string{"slaveAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.SlaveAccessList.ValueString()), "type": fmt.Sprintf("\"%s\"", data.AccessConfiguration.Type.ValueString()), "domain": fmt.Sprintf("\"%s\"", data.AccessConfiguration.Domain.ValueString()), "masterIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.SlaveIpSpace.ValueString())}), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 2, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 2, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *TemplateResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state TemplateModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 2, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan TemplateModel + var state TemplateModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 816: resources_core.BuildJSON(map[string]string{"memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64())}), + 817: resources_core.BuildJSON(map[string]string{"type": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.Type.ValueString()), "domain": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.Domain.ValueString()), "masterIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.SlaveIpSpace.ValueString()), "slaveAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.SlaveAccessList.ValueString())}), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 2, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *TemplateResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state TemplateModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *TemplateResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *TemplateResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/50_nextcloud_backup_resource.go b/generated/prod/go/50_nextcloud_backup_resource.go new file mode 100644 index 0000000..feb1f0e --- /dev/null +++ b/generated/prod/go/50_nextcloud_backup_resource.go @@ -0,0 +1,344 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: nextcloud +// Subresource: backup + +var _ resource.Resource = &NextcloudBackupResource{} + +// NextcloudBackupResource manages a subresource via operations. +type NextcloudBackupResource struct { + client *core.UniversalClient +} + +type NextcloudBackupModel struct { + ID types.String `tfsdk:"id"` + NextcloudID types.String `tfsdk:"nextcloud_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + StartupConfiguration types.String `tfsdk:"startup_configuration"` +} + +func NewNextcloudBackupResource() resource.Resource { + return &NextcloudBackupResource{} +} + +func (r *NextcloudBackupResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_nextcloud_backup" +} + +func (r *NextcloudBackupResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "nextcloud_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "startup_configuration": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *NextcloudBackupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan NextcloudBackupModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if plan.StartupConfiguration.IsNull() || plan.StartupConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "startup_configuration is required.") + return + } + + instanceUID := strings.TrimSpace(plan.NextcloudID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "startupConfiguration": resources_core.FormatString(plan.StartupConfiguration), + } + identityCodes := []string{ + "startupConfiguration", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("backup", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "startupConfiguration": resources_core.FormatString(plan.StartupConfiguration), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_backup", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_backup", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *NextcloudBackupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state NextcloudBackupModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *NextcloudBackupResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan NextcloudBackupModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state NextcloudBackupModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.NextcloudID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{ + "startupConfiguration": resources_core.FormatString(plan.StartupConfiguration), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *NextcloudBackupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state NextcloudBackupModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + return + } + + instanceUID := strings.TrimSpace(state.NextcloudID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "startupConfiguration": resources_core.FormatString(state.StartupConfiguration), + } + identityCodes := []string{ + "startupConfiguration", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("backup", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{}) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *NextcloudBackupResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/50_nextcloud_resource.go b/generated/prod/go/50_nextcloud_resource.go new file mode 100644 index 0000000..94badb7 --- /dev/null +++ b/generated/prod/go/50_nextcloud_resource.go @@ -0,0 +1,414 @@ +package resources_gen + +import ( + "context" + "fmt" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: nextcloud +// Service ID: 50 + +var _ resource.Resource = &NextcloudResource{} +var _ resource.ResourceWithModifyPlan = &NextcloudResource{} +var _ resource.ResourceWithImportState = &NextcloudResource{} + +type NextcloudResource struct { + client *core.UniversalClient +} + +// NextcloudAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration. +type NextcloudAccessConfigurationModel struct { + Domain types.String `tfsdk:"domain" json:"domain"` +} + +// NextcloudAppConfigurationModel — вложенная модель для map-fixed параметра appConfiguration. +type NextcloudAppConfigurationModel struct { + Version types.String `tfsdk:"version" json:"version"` + DocumentServer types.String `tfsdk:"document_server" json:"documentServer"` + LogLevel types.String `tfsdk:"log_level" json:"logLevel"` +} + +// NextcloudClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type NextcloudClusterConfigurationModel struct { + Cpu types.Int64 `tfsdk:"cpu" json:"cpu"` + Memory types.Int64 `tfsdk:"memory" json:"memory"` + Replicas types.Int64 `tfsdk:"replicas" json:"replicas"` + Disk types.Int64 `tfsdk:"disk" json:"disk"` +} + +// NextcloudStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type NextcloudStartupConfigurationModel struct { + ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"` + PsqlEnabled types.Bool `tfsdk:"psql_enabled" json:"psqlEnabled"` + RedisEnabled types.Bool `tfsdk:"redis_enabled" json:"redisEnabled"` + PsqlUid types.String `tfsdk:"psql_uid" json:"psqlUid"` + RedisUid types.String `tfsdk:"redis_uid" json:"redisUid"` +} + +type NextcloudModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessConfiguration *NextcloudAccessConfigurationModel `tfsdk:"access_configuration"` + AppConfiguration *NextcloudAppConfigurationModel `tfsdk:"app_configuration"` + ClusterConfiguration *NextcloudClusterConfigurationModel `tfsdk:"cluster_configuration"` + StartupConfiguration *NextcloudStartupConfigurationModel `tfsdk:"startup_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewNextcloudResource() resource.Resource { + return &NextcloudResource{} +} + +func (r *NextcloudResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_nextcloud" +} + +func (r *NextcloudResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("nextcloud-f6pb9obd"), MarkdownDescription: "Если указан FQDN в формате: `myapp.example.com`, предварительно будет проверяться DNS-запись (А-запись **должна смотреть на IP Ingress кластера ресурсной платформы**)

Если указан в формате `myapp`, DNS-запись будет автоматически создана в служебной DNS-зоне"}, + }}, + "app_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("33.0.4")}, + "document_server": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("collabora"), MarkdownDescription: "Выбор сервера документов (onlyoffice)/(collabora). onlyoffice пока не доступен
Размер пода collabora:
1.8 ядра. 2Gi ОЗУ"}, + "log_level": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("INFO"), MarkdownDescription: "Включает отображение логов самого nextcloud
Логи уходят в /dev/stdout контейнера
`NONE` отключает логирование"}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1000), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"}, + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1024), MarkdownDescription: "Указывается в Megabytes"}, + "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)}, + "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Указывается в GIGAbytes
На данном диске хранятся данные Nextcloud"}, + }}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8s-4-ext-nubes-ru"), MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"}, + "psql_enabled": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Инстанс обязан быть развёрнут на той же ресурсной платформе, где и будет располагаться Nextcloud"}, + "redis_enabled": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Инстанс обязан быть развёрнут на той же ресурсной платформе, где и будет располагаться Nextcloud"}, + "psql_uid": schema.StringAttribute{Required: true, MarkdownDescription: "Необходимо выбирать, когда выбран параметр `Подключить PostgreSQL из Личного кабинета`"}, + "redis_uid": schema.StringAttribute{Required: true, MarkdownDescription: "Необходимо выбирать, когда выбран параметр `Подключить Redis из Личного кабинета`"}, + }}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *NextcloudResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *NextcloudModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *NextcloudModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan NextcloudModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 1055: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString()), "psqlEnabled": fmt.Sprintf("%v", config.StartupConfiguration.PsqlEnabled.ValueBool()), "redisEnabled": fmt.Sprintf("%v", config.StartupConfiguration.RedisEnabled.ValueBool()), "psqlUid": fmt.Sprintf("\"%s\"", config.StartupConfiguration.PsqlUid.ValueString()), "redisUid": fmt.Sprintf("\"%s\"", config.StartupConfiguration.RedisUid.ValueString())}), + 1056: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64())}), + 1057: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", config.AccessConfiguration.Domain.ValueString())}), + 1058: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.AppConfiguration.Version.ValueString()), "documentServer": fmt.Sprintf("\"%s\"", config.AppConfiguration.DocumentServer.ValueString()), "logLevel": fmt.Sprintf("\"%s\"", config.AppConfiguration.LogLevel.ValueString())}), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 50, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *NextcloudResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data NextcloudModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 50, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 1055: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString()), "psqlEnabled": fmt.Sprintf("%v", data.StartupConfiguration.PsqlEnabled.ValueBool()), "redisEnabled": fmt.Sprintf("%v", data.StartupConfiguration.RedisEnabled.ValueBool()), "psqlUid": fmt.Sprintf("\"%s\"", data.StartupConfiguration.PsqlUid.ValueString()), "redisUid": fmt.Sprintf("\"%s\"", data.StartupConfiguration.RedisUid.ValueString())}), + 1056: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64())}), + 1057: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", data.AccessConfiguration.Domain.ValueString())}), + 1058: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.AppConfiguration.Version.ValueString()), "documentServer": fmt.Sprintf("\"%s\"", data.AppConfiguration.DocumentServer.ValueString()), "logLevel": fmt.Sprintf("\"%s\"", data.AppConfiguration.LogLevel.ValueString())}), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 50, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 50, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *NextcloudResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state NextcloudModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 50, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *NextcloudResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan NextcloudModel + var state NextcloudModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 1059: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64())}), + 1060: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", plan.AppConfiguration.Version.ValueString()), "documentServer": fmt.Sprintf("\"%s\"", plan.AppConfiguration.DocumentServer.ValueString()), "logLevel": fmt.Sprintf("\"%s\"", plan.AppConfiguration.LogLevel.ValueString())}), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 50, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *NextcloudResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state NextcloudModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *NextcloudResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *NextcloudResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/81_superset_resource.go b/generated/prod/go/81_superset_resource.go new file mode 100644 index 0000000..0e08f09 --- /dev/null +++ b/generated/prod/go/81_superset_resource.go @@ -0,0 +1,436 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: superset +// Service ID: 81 + +var _ resource.Resource = &SupersetResource{} +var _ resource.ResourceWithModifyPlan = &SupersetResource{} +var _ resource.ResourceWithImportState = &SupersetResource{} + +type SupersetResource struct { + client *core.UniversalClient +} + +type SupersetModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + Domain types.String `tfsdk:"domain"` + Emails types.String `tfsdk:"emails"` + ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"` + ResourceDisk types.Int64 `tfsdk:"resource_disk"` + ResourceInstances types.Int64 `tfsdk:"resource_instances"` + ResourceMemory types.Int64 `tfsdk:"resource_memory"` + ResourceRealm types.String `tfsdk:"resource_realm"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewSupersetResource() resource.Resource { + return &SupersetResource{} +} + +func (r *SupersetResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_superset" +} + +func (r *SupersetResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("superset-1"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."}, + "emails": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("testuser1"), MarkdownDescription: "Учётная запись, которой будут выданы админские права. В формате email [ну не совсем. Для доменных учетных записей - без доменного суффикса]. Обязана находиться в Keycloak. Можно передавать несколько через пробел Не изменяется после создания."}, + "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Квота ядра пода (Указывается в mili). 1000 Mili = 1 ядро Не изменяется после создания."}, + "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Квота диска (Указывается в GIGAbytes) Не изменяется после создания."}, + "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Количество узлов Не изменяется после создания."}, + "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Квота памяти пода (Указывается в Megabytes) Не изменяется после создания."}, + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8s-4-ext-nubes-ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *SupersetResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *SupersetModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *SupersetModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan SupersetModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.Emails.IsNull() && !plan.Emails.IsUnknown() && !state.Emails.IsNull() && !state.Emails.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Emails.ValueString(), state.Emails.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить emails", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() { + if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_c_p_u", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() { + if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_disk", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() { + if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_instances", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() { + if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_memory", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 14: resources_core.FormatString(config.Domain), + 20: resources_core.FormatString(config.Emails), + 96: resources_core.FormatInt64(config.ResourceCPU), + 97: resources_core.FormatInt64(config.ResourceMemory), + 98: resources_core.FormatInt64(config.ResourceDisk), + 109: resources_core.FormatString(config.ResourceRealm), + 227: resources_core.FormatInt64(config.ResourceInstances), + } + desiredDomain := "" + if !config.Domain.IsNull() && !config.Domain.IsUnknown() { + desiredDomain = config.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 81, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *SupersetResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data SupersetModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + if !data.Domain.IsNull() && !data.Domain.IsUnknown() { + desiredDomain = data.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 81, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 14: resources_core.FormatString(data.Domain), + 20: resources_core.FormatString(data.Emails), + 96: resources_core.FormatInt64(data.ResourceCPU), + 97: resources_core.FormatInt64(data.ResourceMemory), + 98: resources_core.FormatInt64(data.ResourceDisk), + 109: resources_core.FormatString(data.ResourceRealm), + 227: resources_core.FormatInt64(data.ResourceInstances), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 81, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 81, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "emails", Field: "Emails", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *SupersetResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state SupersetModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 81, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "emails", Field: "Emails", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *SupersetResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan SupersetModel + var state SupersetModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 81, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "emails", Field: "Emails", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *SupersetResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state SupersetModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *SupersetResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *SupersetResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/82_harbor_resource.go b/generated/prod/go/82_harbor_resource.go new file mode 100644 index 0000000..63306e1 --- /dev/null +++ b/generated/prod/go/82_harbor_resource.go @@ -0,0 +1,458 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: harbor +// Service ID: 82 + +var _ resource.Resource = &HarborResource{} +var _ resource.ResourceWithModifyPlan = &HarborResource{} +var _ resource.ResourceWithImportState = &HarborResource{} + +type HarborResource struct { + client *core.UniversalClient +} + +type HarborModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + Domain types.String `tfsdk:"domain"` + ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"` + ResourceInstances types.Int64 `tfsdk:"resource_instances"` + ResourceMemory types.Int64 `tfsdk:"resource_memory"` + ResourceRealm types.String `tfsdk:"resource_realm"` + S3Uid types.String `tfsdk:"s3_uid"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewHarborResource() resource.Resource { + return &HarborResource{} +} + +func (r *HarborResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_harbor" +} + +func (r *HarborResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("harbor-1"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."}, + "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100), MarkdownDescription: "Квота ядра пода (Указывается в mili). 1000Mili = 1 Ядро Не изменяется после создания."}, + "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Количество узлов Не изменяется после создания."}, + "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(200), MarkdownDescription: "Квота памяти пода (Указывается в Megabytes) Не изменяется после создания."}, + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8s-4-ext-nubes-ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + "s3_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UUID экземпляра услуги S3 Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *HarborResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *HarborModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *HarborModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan HarborModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan. + // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов. + // Resolve (displayName → UUID или uppercase → lowercase) нужен только для + // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка + // "Provider produced invalid plan: planned value does not match config value". + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() { + if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_c_p_u", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() { + if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_instances", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() { + if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_memory", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.S3Uid.IsNull() && !plan.S3Uid.IsUnknown() && !state.S3Uid.IsNull() && !state.S3Uid.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.S3Uid.ValueString(), state.S3Uid.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить s3_uid", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.S3Uid.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "s3_uid is required.") + return + } + if config.S3Uid.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + if !config.S3Uid.IsNull() && !config.S3Uid.IsUnknown() { + resolvedS3Uid, err := r.client.ResolveRefSvcParamValue(ctx, 12, config.S3Uid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve s3_uid", err.Error()) + } else if resolvedS3Uid != "" && resolvedS3Uid != config.S3Uid.ValueString() { + config.S3Uid = types.StringValue(resolvedS3Uid) + } + } + params := map[int]string{ + 16: resources_core.FormatString(config.Domain), + 110: resources_core.FormatString(config.ResourceRealm), + 228: resources_core.FormatInt64(config.ResourceCPU), + 229: resources_core.FormatInt64(config.ResourceMemory), + 230: resources_core.FormatInt64(config.ResourceInstances), + 231: resources_core.FormatString(config.S3Uid), + } + desiredDomain := "" + if !config.Domain.IsNull() && !config.Domain.IsUnknown() { + desiredDomain = config.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 82, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *HarborResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data HarborModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.S3Uid.IsNull() || data.S3Uid.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "s3_uid is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + if !data.Domain.IsNull() && !data.Domain.IsUnknown() { + desiredDomain = data.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 82, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 16: resources_core.FormatString(data.Domain), + 110: resources_core.FormatString(data.ResourceRealm), + 228: resources_core.FormatInt64(data.ResourceCPU), + 229: resources_core.FormatInt64(data.ResourceMemory), + 230: resources_core.FormatInt64(data.ResourceInstances), + 231: resources_core.FormatString(data.S3Uid), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 82, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 82, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + // Restore user-provided casing в state (Create). + // EqualFold = «совпадают ли значения без учёта регистра?» + // Если да — значит API вернул «туже» строку, только в другом регистре. + // Заменяем state на original (то что было в plan/config пользователя). + // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint. + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *HarborResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state HarborModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 82, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + // Restore user-provided casing в state (Read). + // При чтении у нас нет plan — но предыдущий state уже хранит значение + // в регистре пользователя (после первого Create оно было восстановлено). + // Берём prior state (переменная state) как эталон регистра. + // Если API вернул то же UUID только строчными буквами — восстанавливаем. + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *HarborResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan HarborModel + var state HarborModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 82, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + // Restore user-provided casing в state (Update). + // После API-вызова modify state содержит значения в lower-case от API. + // Plan == config == то что написал пользователь (регистр неизменён). + // EqualFold: если UUID совпадает без учёта регистра — берём из plan. + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *HarborResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state HarborModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *HarborResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *HarborResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/86_k8s_velero_resource.go b/generated/prod/go/86_k8s_velero_resource.go new file mode 100644 index 0000000..950e65b --- /dev/null +++ b/generated/prod/go/86_k8s_velero_resource.go @@ -0,0 +1,402 @@ +package resources_gen + +import ( + "context" + "fmt" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: k8s_velero +// Service ID: 86 + +var _ resource.Resource = &K8sVeleroResource{} +var _ resource.ResourceWithModifyPlan = &K8sVeleroResource{} +var _ resource.ResourceWithImportState = &K8sVeleroResource{} + +type K8sVeleroResource struct { + client *core.UniversalClient +} + +// K8sVeleroBackupConfigurationModel — вложенная модель для map-fixed параметра backupConfiguration. +type K8sVeleroBackupConfigurationModel struct { + S3BucketUid types.String `tfsdk:"s3_bucket_uid" json:"s3BucketUid"` +} + +// K8sVeleroClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type K8sVeleroClusterConfigurationModel struct { + Cpu types.Int64 `tfsdk:"cpu" json:"cpu"` + Memory types.Int64 `tfsdk:"memory" json:"memory"` + Replicas types.Int64 `tfsdk:"replicas" json:"replicas"` +} + +// K8sVeleroNodeAgentConfigurationModel — вложенная модель для map-fixed параметра nodeAgentConfiguration. +type K8sVeleroNodeAgentConfigurationModel struct { + Cpu types.Int64 `tfsdk:"cpu" json:"cpu"` + Memory types.Int64 `tfsdk:"memory" json:"memory"` +} + +// K8sVeleroStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type K8sVeleroStartupConfigurationModel struct { + ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"` +} + +type K8sVeleroModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + BackupConfiguration *K8sVeleroBackupConfigurationModel `tfsdk:"backup_configuration"` + ClusterConfiguration *K8sVeleroClusterConfigurationModel `tfsdk:"cluster_configuration"` + NodeAgentConfiguration *K8sVeleroNodeAgentConfigurationModel `tfsdk:"node_agent_configuration"` + StartupConfiguration *K8sVeleroStartupConfigurationModel `tfsdk:"startup_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewK8sVeleroResource() resource.Resource { + return &K8sVeleroResource{} +} + +func (r *K8sVeleroResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_k8s_velero" +} + +func (r *K8sVeleroResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "backup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "s3_bucket_uid": schema.StringAttribute{Required: true, MarkdownDescription: "Экземпляр S3-Бакета обязателен
Учётная запись будет создана автоматически, если её нету"}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1000), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"}, + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2048), MarkdownDescription: "Указывается в Megabytes"}, + "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)}, + }}, + "node_agent_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2000), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"}, + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2048), MarkdownDescription: "Указывается в Megabytes"}, + }}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8s-4-ext-nubes-ru")}, + }}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *K8sVeleroResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *K8sVeleroModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *K8sVeleroModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan K8sVeleroModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 962: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64())}), + 963: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}), + 964: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.NodeAgentConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.NodeAgentConfiguration.Memory.ValueInt64())}), + 1076: resources_core.BuildJSON(map[string]string{"s3BucketUid": fmt.Sprintf("\"%s\"", config.BackupConfiguration.S3BucketUid.ValueString())}), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 86, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *K8sVeleroResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data K8sVeleroModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 86, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 962: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64())}), + 963: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}), + 964: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.NodeAgentConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.NodeAgentConfiguration.Memory.ValueInt64())}), + 1076: resources_core.BuildJSON(map[string]string{"s3BucketUid": fmt.Sprintf("\"%s\"", data.BackupConfiguration.S3BucketUid.ValueString())}), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 86, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 86, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "nodeAgentConfiguration", Field: "NodeAgentConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *K8sVeleroResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state K8sVeleroModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 86, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "nodeAgentConfiguration", Field: "NodeAgentConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *K8sVeleroResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan K8sVeleroModel + var state K8sVeleroModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 965: resources_core.BuildJSON(map[string]string{"memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64())}), + 966: resources_core.BuildJSON(map[string]string{"memory": fmt.Sprintf("%d", plan.NodeAgentConfiguration.Memory.ValueInt64()), "cpu": fmt.Sprintf("%d", plan.NodeAgentConfiguration.Cpu.ValueInt64())}), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 86, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "nodeAgentConfiguration", Field: "NodeAgentConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *K8sVeleroResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state K8sVeleroModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *K8sVeleroResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *K8sVeleroResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/88_k8s_ziti_controller_resource.go b/generated/prod/go/88_k8s_ziti_controller_resource.go new file mode 100644 index 0000000..a3dd491 --- /dev/null +++ b/generated/prod/go/88_k8s_ziti_controller_resource.go @@ -0,0 +1,420 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: k8s_ziti_controller +// Service ID: 88 + +var _ resource.Resource = &K8sZitiControllerResource{} +var _ resource.ResourceWithModifyPlan = &K8sZitiControllerResource{} +var _ resource.ResourceWithImportState = &K8sZitiControllerResource{} + +type K8sZitiControllerResource struct { + client *core.UniversalClient +} + +type K8sZitiControllerModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + Domain types.String `tfsdk:"domain"` + ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"` + ResourceDisk types.Int64 `tfsdk:"resource_disk"` + ResourceInstances types.Int64 `tfsdk:"resource_instances"` + ResourceMemory types.Int64 `tfsdk:"resource_memory"` + ResourceRealm types.String `tfsdk:"resource_realm"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewK8sZitiControllerResource() resource.Resource { + return &K8sZitiControllerResource{} +} + +func (r *K8sZitiControllerResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_k8s_ziti_controller" +} + +func (r *K8sZitiControllerResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8sZitiController-1"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."}, + "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(300), MarkdownDescription: "Квота ядра пода (milicores) Не изменяется после создания."}, + "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Квота диска (Указывается в GIGAbytes) Не изменяется после создания."}, + "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Количество узлов Не изменяется после создания."}, + "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Квота памяти пода (MBytes) Не изменяется после создания."}, + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8s-4-ext-nubes-ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *K8sZitiControllerResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *K8sZitiControllerModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *K8sZitiControllerModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan K8sZitiControllerModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() { + if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_c_p_u", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() { + if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_disk", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() { + if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_instances", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() { + if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_memory", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 294: resources_core.FormatInt64(config.ResourceCPU), + 295: resources_core.FormatInt64(config.ResourceMemory), + 296: resources_core.FormatInt64(config.ResourceDisk), + 297: resources_core.FormatInt64(config.ResourceInstances), + 298: resources_core.FormatString(config.ResourceRealm), + 299: resources_core.FormatString(config.Domain), + } + desiredDomain := "" + if !config.Domain.IsNull() && !config.Domain.IsUnknown() { + desiredDomain = config.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 88, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *K8sZitiControllerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data K8sZitiControllerModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + if !data.Domain.IsNull() && !data.Domain.IsUnknown() { + desiredDomain = data.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 88, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 294: resources_core.FormatInt64(data.ResourceCPU), + 295: resources_core.FormatInt64(data.ResourceMemory), + 296: resources_core.FormatInt64(data.ResourceDisk), + 297: resources_core.FormatInt64(data.ResourceInstances), + 298: resources_core.FormatString(data.ResourceRealm), + 299: resources_core.FormatString(data.Domain), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 88, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 88, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *K8sZitiControllerResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state K8sZitiControllerModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 88, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *K8sZitiControllerResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan K8sZitiControllerModel + var state K8sZitiControllerModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 88, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *K8sZitiControllerResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state K8sZitiControllerModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *K8sZitiControllerResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *K8sZitiControllerResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/89_flask_resource.go b/generated/prod/go/89_flask_resource.go new file mode 100644 index 0000000..0951b0b --- /dev/null +++ b/generated/prod/go/89_flask_resource.go @@ -0,0 +1,449 @@ +package resources_gen + +import ( + "context" + "fmt" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: flask +// Service ID: 89 + +var _ resource.Resource = &FlaskResource{} +var _ resource.ResourceWithModifyPlan = &FlaskResource{} +var _ resource.ResourceWithImportState = &FlaskResource{} + +type FlaskResource struct { + client *core.UniversalClient +} + +// FlaskAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration. +type FlaskAccessConfigurationModel struct { + Domain types.String `tfsdk:"domain" json:"domain"` +} + +// FlaskAppConfigurationModel — вложенная модель для map-fixed параметра appConfiguration. +type FlaskAppConfigurationModel struct { + Version types.String `tfsdk:"version" json:"version"` + GitPath types.String `tfsdk:"git_path" json:"gitPath"` + HealthPath types.String `tfsdk:"health_path" json:"healthPath"` +} + +// FlaskClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type FlaskClusterConfigurationModel struct { + Cpu types.Int64 `tfsdk:"cpu" json:"cpu"` + Memory types.Int64 `tfsdk:"memory" json:"memory"` + Replicas types.Int64 `tfsdk:"replicas" json:"replicas"` +} + +// FlaskStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type FlaskStartupConfigurationModel struct { + ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"` +} + +type FlaskModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + // --- Redeploy support (ARCHITECTURE.md) --- + // git_revision: при изменении вызывает redeploy вместо modify. + // Опциональное поле — если не задано, modify работает как обычно. + GitRevision types.String `tfsdk:"git_revision"` + AccessConfiguration *FlaskAccessConfigurationModel `tfsdk:"access_configuration"` + AppConfiguration *FlaskAppConfigurationModel `tfsdk:"app_configuration"` + ClusterConfiguration *FlaskClusterConfigurationModel `tfsdk:"cluster_configuration"` + JsonEnv types.String `tfsdk:"json_env"` + StartupConfiguration *FlaskStartupConfigurationModel `tfsdk:"startup_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewFlaskResource() resource.Resource { + return &FlaskResource{} +} + +func (r *FlaskResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_flask" +} + +func (r *FlaskResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("flask-whmcbvdz"), MarkdownDescription: "Если указан FQDN в формате: `myapp.example.com`, предварительно будет проверяться DNS-запись (А-запись **должна смотреть на IP Ingress кластера ресурсной платформы**)

Если указан в формате `myapp`, DNS-запись будет автоматически создана в служебной DNS-зоне"}, + }}, + "app_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("3.12")}, + "git_path": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("https://github.com/Foxyhhd/Baldurs-Gate-test.git"), MarkdownDescription: "Путь до репозитория приложения
Обязательно должен быть передан как URL: `https://....`

Должен быть сетевой доступ до URL для скачивания исходников"}, + "health_path": schema.StringAttribute{Optional: true, MarkdownDescription: "Путь до healtcheck для запуска пода в Kubernetes, который отдаст 200 код
Пустое значение означает, что будет проверяться исключительно доступность приложения по порту"}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(200), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"}, + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(256), MarkdownDescription: "Указывается в Megabytes"}, + "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)}, + }}, + "json_env": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("{}")}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8s-4-ext-nubes-ru"), MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"}, + }}, + // --- Redeploy support (ARCHITECTURE.md) --- + // При изменении git_revision вызывается redeploy вместо modify. + // Если не задано — modify работает без изменений. + "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *FlaskResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *FlaskModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *FlaskModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan FlaskModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 1037: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}), + 1038: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64())}), + 1039: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", config.AccessConfiguration.Domain.ValueString())}), + 1040: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", config.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", config.AppConfiguration.HealthPath.ValueString())}), + 1041: resources_core.FormatString(config.JsonEnv), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 89, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *FlaskResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data FlaskModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 89, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 1037: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}), + 1038: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64())}), + 1039: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", data.AccessConfiguration.Domain.ValueString())}), + 1040: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", data.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", data.AppConfiguration.HealthPath.ValueString())}), + 1041: resources_core.FormatString(data.JsonEnv), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 89, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 89, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "jsonEnv", Field: "JsonEnv", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *FlaskResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state FlaskModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 89, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "jsonEnv", Field: "JsonEnv", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *FlaskResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan FlaskModel + var state FlaskModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.JsonEnv.IsNull() != state.JsonEnv.IsNull() || plan.JsonEnv.IsUnknown() != state.JsonEnv.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.JsonEnv.IsNull() && !plan.JsonEnv.IsUnknown() && !state.JsonEnv.IsNull() && !state.JsonEnv.IsUnknown() { + if !strings.EqualFold(plan.JsonEnv.ValueString(), state.JsonEnv.ValueString()) { + hasServiceParamChanges = true + } + } + } + // --- Redeploy support (ARCHITECTURE.md) --- + // Проверяем изменился ли git_revision. + // Если да — вызываем redeploy вместо modify. + // git_revision = "" (не задано) → обычный modify. + redeployRequested := false + if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() { + if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() { + redeployRequested = true + } + } + + if !hasServiceParamChanges && !redeployRequested { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + // --- Redeploy support (ARCHITECTURE.md) --- + if redeployRequested { + // Вызываем redeploy — пустой params (операция не требует CFS-параметров) + if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + } else { + params := map[int]string{ + 1042: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64())}), + 1043: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", plan.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", plan.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", plan.AppConfiguration.HealthPath.ValueString())}), + 1044: resources_core.FormatString(plan.JsonEnv), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 89, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "jsonEnv", Field: "JsonEnv", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *FlaskResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state FlaskModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *FlaskResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *FlaskResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/90_postgres_backup_resource.go b/generated/prod/go/90_postgres_backup_resource.go new file mode 100644 index 0000000..957eb3e --- /dev/null +++ b/generated/prod/go/90_postgres_backup_resource.go @@ -0,0 +1,326 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: postgres +// Subresource: backup + +var _ resource.Resource = &PostgresBackupResource{} + +// PostgresBackupResource manages a subresource via operations. +type PostgresBackupResource struct { + client *core.UniversalClient +} + +type PostgresBackupModel struct { + ID types.String `tfsdk:"id"` + PostgresID types.String `tfsdk:"postgres_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` +} + +func NewPostgresBackupResource() resource.Resource { + return &PostgresBackupResource{} +} + +func (r *PostgresBackupResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_postgres_backup" +} + +func (r *PostgresBackupResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "postgres_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *PostgresBackupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan PostgresBackupModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.PostgresID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{} + identityCodes := []string{} + listKey, idKey := resources_core.ResolveSubresourceStateKeys("backup", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{}) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_backup", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_backup", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *PostgresBackupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state PostgresBackupModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *PostgresBackupResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan PostgresBackupModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state PostgresBackupModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.PostgresID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{} + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *PostgresBackupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state PostgresBackupModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + return + } + + instanceUID := strings.TrimSpace(state.PostgresID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{} + identityCodes := []string{} + listKey, idKey := resources_core.ResolveSubresourceStateKeys("backup", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{}) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *PostgresBackupResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/90_postgres_database_resource.go b/generated/prod/go/90_postgres_database_resource.go new file mode 100644 index 0000000..7310720 --- /dev/null +++ b/generated/prod/go/90_postgres_database_resource.go @@ -0,0 +1,358 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: postgres +// Subresource: database + +var _ resource.Resource = &PostgresDatabaseResource{} + +// PostgresDatabaseResource manages a subresource via operations. +type PostgresDatabaseResource struct { + client *core.UniversalClient +} + +type PostgresDatabaseModel struct { + ID types.String `tfsdk:"id"` + PostgresID types.String `tfsdk:"postgres_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + DbName types.String `tfsdk:"db_name"` + DbOwner types.String `tfsdk:"db_owner"` +} + +func NewPostgresDatabaseResource() resource.Resource { + return &PostgresDatabaseResource{} +} + +func (r *PostgresDatabaseResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_postgres_database" +} + +func (r *PostgresDatabaseResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "postgres_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "db_name": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "db_owner": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *PostgresDatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan PostgresDatabaseModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if plan.DbName.IsNull() || plan.DbName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "db_name is required.") + return + } + if plan.DbOwner.IsNull() || plan.DbOwner.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "db_owner is required.") + return + } + + instanceUID := strings.TrimSpace(plan.PostgresID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "dbName": resources_core.FormatString(plan.DbName), + "dbOwner": resources_core.FormatString(plan.DbOwner), + } + identityCodes := []string{ + "dbName", + "dbOwner", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("database", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "dbName": resources_core.FormatString(plan.DbName), + "dbOwner": resources_core.FormatString(plan.DbOwner), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_database", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_database", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *PostgresDatabaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state PostgresDatabaseModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *PostgresDatabaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan PostgresDatabaseModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state PostgresDatabaseModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.PostgresID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{ + "dbName": resources_core.FormatString(plan.DbName), + "dbOwner": resources_core.FormatString(plan.DbOwner), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *PostgresDatabaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state PostgresDatabaseModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_database" == "" { + return + } + + instanceUID := strings.TrimSpace(state.PostgresID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "dbName": resources_core.FormatString(state.DbName), + "dbOwner": resources_core.FormatString(state.DbOwner), + } + identityCodes := []string{ + "dbName", + "dbOwner", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("database", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "dbName": resources_core.FormatString(state.DbName), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_database", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *PostgresDatabaseResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/90_postgres_resource.go b/generated/prod/go/90_postgres_resource.go new file mode 100644 index 0000000..6ef10dd --- /dev/null +++ b/generated/prod/go/90_postgres_resource.go @@ -0,0 +1,475 @@ +package resources_gen + +import ( + "context" + "fmt" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: postgres +// Service ID: 90 + +var _ resource.Resource = &PostgresResource{} +var _ resource.ResourceWithModifyPlan = &PostgresResource{} +var _ resource.ResourceWithImportState = &PostgresResource{} + +type PostgresResource struct { + client *core.UniversalClient +} + +// PostgresAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration. +type PostgresAccessConfigurationModel struct { + SlaveIpSpace types.String `tfsdk:"slave_ip_space" json:"slaveIpSpace"` + SlaveAccessList types.String `tfsdk:"slave_access_list" json:"slaveAccessList"` + MasterIpSpace types.String `tfsdk:"master_ip_space" json:"masterIpSpace"` + MasterAccessList types.String `tfsdk:"master_access_list" json:"masterAccessList"` +} + +// PostgresAutoscaleConfigurationModel — вложенная модель для map-fixed параметра autoscaleConfiguration. +type PostgresAutoscaleConfigurationModel struct { + Schedule types.Int64 `tfsdk:"schedule" json:"schedule"` + Quota types.Int64 `tfsdk:"quota" json:"quota"` + Percent types.Int64 `tfsdk:"percent" json:"percent"` + Enabled types.Bool `tfsdk:"enabled" json:"enabled"` +} + +// PostgresBackupConfigurationModel — вложенная модель для map-fixed параметра backupConfiguration. +type PostgresBackupConfigurationModel struct { + S3Uid types.String `tfsdk:"s3_uid" json:"s3Uid"` + Retain types.Int64 `tfsdk:"retain" json:"retain"` + Schedule types.String `tfsdk:"schedule" json:"schedule"` +} + +// PostgresClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type PostgresClusterConfigurationModel struct { + Cpu types.Int64 `tfsdk:"cpu" json:"cpu"` + Memory types.Int64 `tfsdk:"memory" json:"memory"` + Replicas types.Int64 `tfsdk:"replicas" json:"replicas"` + Disk types.Int64 `tfsdk:"disk" json:"disk"` +} + +// PostgresPostgresConfigurationModel — вложенная модель для map-fixed параметра postgresConfiguration. +type PostgresPostgresConfigurationModel struct { + Version types.String `tfsdk:"version" json:"version"` + SslRequired types.Bool `tfsdk:"ssl_required" json:"sslRequired"` + PoolerMaster types.Bool `tfsdk:"pooler_master" json:"poolerMaster"` + PoolerSlave types.Bool `tfsdk:"pooler_slave" json:"poolerSlave"` +} + +// PostgresStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type PostgresStartupConfigurationModel struct { + ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"` +} + +type PostgresModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessConfiguration *PostgresAccessConfigurationModel `tfsdk:"access_configuration"` + AutoscaleConfiguration *PostgresAutoscaleConfigurationModel `tfsdk:"autoscale_configuration"` + BackupConfiguration *PostgresBackupConfigurationModel `tfsdk:"backup_configuration"` + ClusterConfiguration *PostgresClusterConfigurationModel `tfsdk:"cluster_configuration"` + PostgresConf types.String `tfsdk:"postgres_conf"` + PostgresConfiguration *PostgresPostgresConfigurationModel `tfsdk:"postgres_configuration"` + StartupConfiguration *PostgresStartupConfigurationModel `tfsdk:"startup_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewPostgresResource() resource.Resource { + return &PostgresResource{} +} + +func (r *PostgresResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_postgres" +} + +func (r *PostgresResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "slave_ip_space": schema.StringAttribute{Required: true, MarkdownDescription: "Из какого Ip-Space резервировать внешний IP
Список предоставляется из Организации, в котором развёрнута ресурсная платформа"}, + "slave_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"}, + "master_ip_space": schema.StringAttribute{Required: true, MarkdownDescription: "Из какого Ip-Space резервировать внешний IP
Список предоставляется из Организации, в котором развёрнута ресурсная платформа"}, + "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"}, + }}, + "autoscale_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "schedule": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(0), MarkdownDescription: "Требует включения autoScale. Окно обновления (пока не реализовано). Указывается час в виде Integer (0 / 5 / 12 / 23)"}, + "quota": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100), MarkdownDescription: "До какого размера возможно увеличивать диск, когда включен Autoscale"}, + "percent": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Требует включения autoScale. Позволяет указать в процентах расширение от текущего макс объема
Минимальный размер на который расширяется - **1Gb**"}, + "enabled": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включает автоскейлинг PV. При включении параметра необходимо настроить autoScalePercentage и autoScaleTechWindow. Алерт при срабатывании вызывает modify у текущей услуги"}, + }}, + "backup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "s3_uid": schema.StringAttribute{Required: true, MarkdownDescription: "Экземпляр S3 обязателен"}, + "retain": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(7)}, + "schedule": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("0 0 * * *")}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"}, + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Указывается в Megabytes"}, + "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)}, + "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Указывается в GIGAbytes"}, + }}, + "postgres_conf": schema.StringAttribute{Optional: true}, + "postgres_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("17")}, + "ssl_required": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "По умолчанию подключение обязательное (sslmode=require)"}, + "pooler_master": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включает pgPooler (на запись). Будет создан новый под
Размер пода **200m/400Mi**"}, + "pooler_slave": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включает pgPooler (на чтение). Будет создан новый под
Размер пода **200m/400Mi**"}, + }}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8s-4-ext-nubes-ru"), MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"}, + }}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *PostgresResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *PostgresModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *PostgresModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan PostgresModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 788: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64())}), + 789: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}), + 790: resources_core.BuildJSON(map[string]string{"slaveIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.SlaveIpSpace.ValueString()), "slaveAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.SlaveAccessList.ValueString()), "masterIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterAccessList.ValueString())}), + 791: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.PostgresConfiguration.Version.ValueString()), "sslRequired": fmt.Sprintf("%v", config.PostgresConfiguration.SslRequired.ValueBool()), "poolerMaster": fmt.Sprintf("%v", config.PostgresConfiguration.PoolerMaster.ValueBool()), "poolerSlave": fmt.Sprintf("%v", config.PostgresConfiguration.PoolerSlave.ValueBool())}), + 792: resources_core.FormatString(config.PostgresConf), + 793: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", config.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", config.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", config.BackupConfiguration.Schedule.ValueString())}), + 794: resources_core.BuildJSON(map[string]string{"schedule": fmt.Sprintf("%d", config.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", config.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", config.AutoscaleConfiguration.Percent.ValueInt64()), "enabled": fmt.Sprintf("%v", config.AutoscaleConfiguration.Enabled.ValueBool())}), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 90, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *PostgresResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data PostgresModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 90, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 788: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64())}), + 789: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}), + 790: resources_core.BuildJSON(map[string]string{"slaveIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.SlaveIpSpace.ValueString()), "slaveAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.SlaveAccessList.ValueString()), "masterIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterAccessList.ValueString())}), + 791: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.PostgresConfiguration.Version.ValueString()), "sslRequired": fmt.Sprintf("%v", data.PostgresConfiguration.SslRequired.ValueBool()), "poolerMaster": fmt.Sprintf("%v", data.PostgresConfiguration.PoolerMaster.ValueBool()), "poolerSlave": fmt.Sprintf("%v", data.PostgresConfiguration.PoolerSlave.ValueBool())}), + 792: resources_core.FormatString(data.PostgresConf), + 793: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", data.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", data.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", data.BackupConfiguration.Schedule.ValueString())}), + 794: resources_core.BuildJSON(map[string]string{"schedule": fmt.Sprintf("%d", data.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", data.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", data.AutoscaleConfiguration.Percent.ValueInt64()), "enabled": fmt.Sprintf("%v", data.AutoscaleConfiguration.Enabled.ValueBool())}), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 90, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 90, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"}, + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "postgresConf", Field: "PostgresConf", Type: "array-map-fixed"}, + {Code: "postgresConfiguration", Field: "PostgresConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *PostgresResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state PostgresModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 90, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"}, + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "postgresConf", Field: "PostgresConf", Type: "array-map-fixed"}, + {Code: "postgresConfiguration", Field: "PostgresConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *PostgresResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan PostgresModel + var state PostgresModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.PostgresConf.IsNull() != state.PostgresConf.IsNull() || plan.PostgresConf.IsUnknown() != state.PostgresConf.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.PostgresConf.IsNull() && !plan.PostgresConf.IsUnknown() && !state.PostgresConf.IsNull() && !state.PostgresConf.IsUnknown() { + if !strings.EqualFold(plan.PostgresConf.ValueString(), state.PostgresConf.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 795: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64())}), + 796: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.SlaveIpSpace.ValueString()), "slaveAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.SlaveAccessList.ValueString())}), + 797: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", plan.PostgresConfiguration.Version.ValueString()), "sslRequired": fmt.Sprintf("%v", plan.PostgresConfiguration.SslRequired.ValueBool()), "poolerMaster": fmt.Sprintf("%v", plan.PostgresConfiguration.PoolerMaster.ValueBool()), "poolerSlave": fmt.Sprintf("%v", plan.PostgresConfiguration.PoolerSlave.ValueBool())}), + 798: resources_core.FormatString(plan.PostgresConf), + 799: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", plan.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", plan.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", plan.BackupConfiguration.Schedule.ValueString())}), + 800: resources_core.BuildJSON(map[string]string{"enabled": fmt.Sprintf("%v", plan.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Percent.ValueInt64())}), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 90, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"}, + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "postgresConf", Field: "PostgresConf", Type: "array-map-fixed"}, + {Code: "postgresConfiguration", Field: "PostgresConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *PostgresResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state PostgresModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *PostgresResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *PostgresResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/90_postgres_user_resource.go b/generated/prod/go/90_postgres_user_resource.go new file mode 100644 index 0000000..5101eca --- /dev/null +++ b/generated/prod/go/90_postgres_user_resource.go @@ -0,0 +1,358 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: postgres +// Subresource: user + +var _ resource.Resource = &PostgresUserResource{} + +// PostgresUserResource manages a subresource via operations. +type PostgresUserResource struct { + client *core.UniversalClient +} + +type PostgresUserModel struct { + ID types.String `tfsdk:"id"` + PostgresID types.String `tfsdk:"postgres_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + Role types.String `tfsdk:"role"` + Username types.String `tfsdk:"username"` +} + +func NewPostgresUserResource() resource.Resource { + return &PostgresUserResource{} +} + +func (r *PostgresUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_postgres_user" +} + +func (r *PostgresUserResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "postgres_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "role": schema.StringAttribute{Required: true, MarkdownDescription: "Выбрать роль из списка Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "username": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *PostgresUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan PostgresUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if plan.Role.IsNull() || plan.Role.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "role is required.") + return + } + if plan.Username.IsNull() || plan.Username.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "username is required.") + return + } + + instanceUID := strings.TrimSpace(plan.PostgresID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "role": resources_core.FormatString(plan.Role), + "username": resources_core.FormatString(plan.Username), + } + identityCodes := []string{ + "role", + "username", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "username": resources_core.FormatString(plan.Username), + "role": resources_core.FormatString(plan.Role), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *PostgresUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state PostgresUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *PostgresUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan PostgresUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state PostgresUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.PostgresID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{ + "role": resources_core.FormatString(plan.Role), + "username": resources_core.FormatString(plan.Username), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *PostgresUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state PostgresUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_user" == "" { + return + } + + instanceUID := strings.TrimSpace(state.PostgresID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "role": resources_core.FormatString(state.Role), + "username": resources_core.FormatString(state.Username), + } + identityCodes := []string{ + "role", + "username", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "username": resources_core.FormatString(state.Username), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_user", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *PostgresUserResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/91_redis_resource.go b/generated/prod/go/91_redis_resource.go new file mode 100644 index 0000000..852aaeb --- /dev/null +++ b/generated/prod/go/91_redis_resource.go @@ -0,0 +1,393 @@ +package resources_gen + +import ( + "context" + "fmt" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: redis +// Service ID: 91 + +var _ resource.Resource = &RedisResource{} +var _ resource.ResourceWithModifyPlan = &RedisResource{} +var _ resource.ResourceWithImportState = &RedisResource{} + +type RedisResource struct { + client *core.UniversalClient +} + +// RedisAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration. +type RedisAccessConfigurationModel struct { + MasterIpSpace types.String `tfsdk:"master_ip_space" json:"masterIpSpace"` + MasterAccessList types.String `tfsdk:"master_access_list" json:"masterAccessList"` + SlaveIpSpace types.String `tfsdk:"slave_ip_space" json:"slaveIpSpace"` + SlaveAccessList types.String `tfsdk:"slave_access_list" json:"slaveAccessList"` +} + +// RedisClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type RedisClusterConfigurationModel struct { + Cpu types.Int64 `tfsdk:"cpu" json:"cpu"` + Memory types.Int64 `tfsdk:"memory" json:"memory"` + Replicas types.Int64 `tfsdk:"replicas" json:"replicas"` + Disk types.Int64 `tfsdk:"disk" json:"disk"` +} + +// RedisStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type RedisStartupConfigurationModel struct { + ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"` +} + +type RedisModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessConfiguration *RedisAccessConfigurationModel `tfsdk:"access_configuration"` + ClusterConfiguration *RedisClusterConfigurationModel `tfsdk:"cluster_configuration"` + StartupConfiguration *RedisStartupConfigurationModel `tfsdk:"startup_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewRedisResource() resource.Resource { + return &RedisResource{} +} + +func (r *RedisResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_redis" +} + +func (r *RedisResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "master_ip_space": schema.StringAttribute{Required: true, MarkdownDescription: "Из какого Ip-Space резервировать внешний IP
Список предоставляется из Организации, в котором развёрнута ресурсная платформа"}, + "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"}, + "slave_ip_space": schema.StringAttribute{Required: true, MarkdownDescription: "Из какого Ip-Space резервировать внешний IP
Список предоставляется из Организации, в котором развёрнута ресурсная платформа"}, + "slave_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"}, + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Указывается в Megabytes"}, + "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)}, + "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Указывается в GIGAbytes"}, + }}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8s-4-ext-nubes-ru"), MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"}, + }}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *RedisResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *RedisModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *RedisModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan RedisModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 1061: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}), + 1062: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64())}), + 1063: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.SlaveIpSpace.ValueString()), "slaveAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.SlaveAccessList.ValueString())}), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 91, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *RedisResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data RedisModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 91, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 1061: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}), + 1062: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64())}), + 1063: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.SlaveIpSpace.ValueString()), "slaveAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.SlaveAccessList.ValueString())}), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 91, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 91, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *RedisResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state RedisModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 91, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *RedisResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan RedisModel + var state RedisModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 1064: resources_core.BuildJSON(map[string]string{"replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64())}), + 1065: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.SlaveIpSpace.ValueString()), "slaveAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.SlaveAccessList.ValueString())}), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 91, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *RedisResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state RedisModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *RedisResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *RedisResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/92_mongodb_resource.go b/generated/prod/go/92_mongodb_resource.go new file mode 100644 index 0000000..e4f0135 --- /dev/null +++ b/generated/prod/go/92_mongodb_resource.go @@ -0,0 +1,495 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: mongodb +// Service ID: 92 + +var _ resource.Resource = &MongodbResource{} +var _ resource.ResourceWithModifyPlan = &MongodbResource{} +var _ resource.ResourceWithImportState = &MongodbResource{} + +type MongodbResource struct { + client *core.UniversalClient +} + +type MongodbModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + IpSpaceNameMaster types.String `tfsdk:"ip_space_name_master"` + MasterAccessList types.String `tfsdk:"master_access_list"` + ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"` + ResourceDisk types.Int64 `tfsdk:"resource_disk"` + ResourceInstances types.Int64 `tfsdk:"resource_instances"` + ResourceMemory types.Int64 `tfsdk:"resource_memory"` + ResourceRealm types.String `tfsdk:"resource_realm"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewMongodbResource() resource.Resource { + return &MongodbResource{} +} + +func (r *MongodbResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_mongodb" +} + +func (r *MongodbResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "ip_space_name_master": schema.StringAttribute{Optional: true, MarkdownDescription: "Если включен предыдущий параметр, то поле необходимо заполнить. Пример: `internet-no-antiddos-v1`"}, + "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Список IP-адресов, с которых разрешён доступ. Если оставить пустым — доступ открыт всем.", PlanModifiers: []planmodifier.String{resources_core.JsonNormalize()}}, + "resource_c_p_u": schema.Int64Attribute{Required: true, MarkdownDescription: "Пример: `1000`"}, + "resource_disk": schema.Int64Attribute{Required: true, MarkdownDescription: "Пример: `20`"}, + "resource_instances": schema.Int64Attribute{Required: true, MarkdownDescription: "Пример: `2`"}, + "resource_memory": schema.Int64Attribute{Required: true, MarkdownDescription: "Пример: `2048`"}, + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8s-4-ext-nubes-ru"), MarkdownDescription: "Выбрать платформу из списка Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *MongodbResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *MongodbModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *MongodbModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan MongodbModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.ResourceCPU.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "resource_c_p_u is required.") + return + } + if config.ResourceCPU.IsUnknown() { + return + } + if config.ResourceDisk.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "resource_disk is required.") + return + } + if config.ResourceDisk.IsUnknown() { + return + } + if config.ResourceInstances.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "resource_instances is required.") + return + } + if config.ResourceInstances.IsUnknown() { + return + } + if config.ResourceMemory.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "resource_memory is required.") + return + } + if config.ResourceMemory.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 487: resources_core.FormatInt64(config.ResourceInstances), + 588: resources_core.FormatInt64(config.ResourceMemory), + 589: resources_core.FormatInt64(config.ResourceCPU), + 590: resources_core.FormatInt64(config.ResourceDisk), + 595: resources_core.FormatString(config.ResourceRealm), + 646: resources_core.FormatString(config.IpSpaceNameMaster), + 889: resources_core.FormatString(config.MasterAccessList), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 92, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *MongodbResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data MongodbModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.ResourceCPU.IsNull() || data.ResourceCPU.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "resource_c_p_u is required.") + return + } + if data.ResourceDisk.IsNull() || data.ResourceDisk.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "resource_disk is required.") + return + } + if data.ResourceInstances.IsNull() || data.ResourceInstances.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "resource_instances is required.") + return + } + if data.ResourceMemory.IsNull() || data.ResourceMemory.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "resource_memory is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 92, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 487: resources_core.FormatInt64(data.ResourceInstances), + 588: resources_core.FormatInt64(data.ResourceMemory), + 589: resources_core.FormatInt64(data.ResourceCPU), + 590: resources_core.FormatInt64(data.ResourceDisk), + 595: resources_core.FormatString(data.ResourceRealm), + 646: resources_core.FormatString(data.IpSpaceNameMaster), + 889: resources_core.FormatString(data.MasterAccessList), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 92, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 92, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "ipSpaceNameMaster", Field: "IpSpaceNameMaster", Type: "string"}, + {Code: "masterAccessList", Field: "MasterAccessList", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *MongodbResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state MongodbModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 92, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "ipSpaceNameMaster", Field: "IpSpaceNameMaster", Type: "string"}, + {Code: "masterAccessList", Field: "MasterAccessList", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *MongodbResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan MongodbModel + var state MongodbModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.ResourceInstances.IsNull() != state.ResourceInstances.IsNull() || plan.ResourceInstances.IsUnknown() != state.ResourceInstances.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() { + if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceMemory.IsNull() != state.ResourceMemory.IsNull() || plan.ResourceMemory.IsUnknown() != state.ResourceMemory.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() { + if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceCPU.IsNull() != state.ResourceCPU.IsNull() || plan.ResourceCPU.IsUnknown() != state.ResourceCPU.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() { + if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceDisk.IsNull() != state.ResourceDisk.IsNull() || plan.ResourceDisk.IsUnknown() != state.ResourceDisk.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() { + if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.IpSpaceNameMaster.IsNull() != state.IpSpaceNameMaster.IsNull() || plan.IpSpaceNameMaster.IsUnknown() != state.IpSpaceNameMaster.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.IpSpaceNameMaster.IsNull() && !plan.IpSpaceNameMaster.IsUnknown() && !state.IpSpaceNameMaster.IsNull() && !state.IpSpaceNameMaster.IsUnknown() { + if !strings.EqualFold(plan.IpSpaceNameMaster.ValueString(), state.IpSpaceNameMaster.ValueString()) { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.MasterAccessList.IsNull() != state.MasterAccessList.IsNull() || plan.MasterAccessList.IsUnknown() != state.MasterAccessList.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.MasterAccessList.IsNull() && !plan.MasterAccessList.IsUnknown() && !state.MasterAccessList.IsNull() && !state.MasterAccessList.IsUnknown() { + if !strings.EqualFold(plan.MasterAccessList.ValueString(), state.MasterAccessList.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 648: resources_core.FormatInt64(plan.ResourceInstances), + 649: resources_core.FormatInt64(plan.ResourceMemory), + 650: resources_core.FormatInt64(plan.ResourceCPU), + 651: resources_core.FormatInt64(plan.ResourceDisk), + 653: resources_core.FormatString(plan.IpSpaceNameMaster), + 890: resources_core.FormatString(plan.MasterAccessList), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 92, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "ipSpaceNameMaster", Field: "IpSpaceNameMaster", Type: "string"}, + {Code: "masterAccessList", Field: "MasterAccessList", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *MongodbResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state MongodbModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *MongodbResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *MongodbResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/92_mongodb_user_resource.go b/generated/prod/go/92_mongodb_user_resource.go new file mode 100644 index 0000000..6f0e40d --- /dev/null +++ b/generated/prod/go/92_mongodb_user_resource.go @@ -0,0 +1,364 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: mongodb +// Subresource: user + +var _ resource.Resource = &MongodbUserResource{} + +// MongodbUserResource manages a subresource via operations. +type MongodbUserResource struct { + client *core.UniversalClient +} + +type MongodbUserModel struct { + ID types.String `tfsdk:"id"` + MongodbID types.String `tfsdk:"mongodb_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + DbName types.String `tfsdk:"db_name"` + Role types.String `tfsdk:"role"` + Username types.String `tfsdk:"username"` +} + +func NewMongodbUserResource() resource.Resource { + return &MongodbUserResource{} +} + +func (r *MongodbUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_mongodb_user" +} + +func (r *MongodbUserResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "mongodb_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "db_name": schema.StringAttribute{Required: true}, + "role": schema.StringAttribute{Required: true}, + "username": schema.StringAttribute{Required: true, MarkdownDescription: "Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *MongodbUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan MongodbUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if plan.DbName.IsNull() || plan.DbName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "db_name is required.") + return + } + if plan.Role.IsNull() || plan.Role.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "role is required.") + return + } + if plan.Username.IsNull() || plan.Username.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "username is required.") + return + } + + instanceUID := strings.TrimSpace(plan.MongodbID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "username": resources_core.FormatString(plan.Username), + } + identityCodes := []string{ + "username", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "username": resources_core.FormatString(plan.Username), + "dbName": resources_core.FormatString(plan.DbName), + "role": resources_core.FormatString(plan.Role), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *MongodbUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state MongodbUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *MongodbUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan MongodbUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "modify_user" == "" { + var state MongodbUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.MongodbID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{ + "username": resources_core.FormatString(plan.Username), + "dbName": resources_core.FormatString(plan.DbName), + "role": resources_core.FormatString(plan.Role), + }) + + idParams := map[string]string{ + "username": resources_core.FormatString(plan.Username), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "modify_user", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *MongodbUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state MongodbUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_user" == "" { + return + } + + instanceUID := strings.TrimSpace(state.MongodbID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "username": resources_core.FormatString(state.Username), + } + identityCodes := []string{ + "username", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "username": resources_core.FormatString(state.Username), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_user", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *MongodbUserResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/93_rabbitmq_resource.go b/generated/prod/go/93_rabbitmq_resource.go new file mode 100644 index 0000000..c26d54b --- /dev/null +++ b/generated/prod/go/93_rabbitmq_resource.go @@ -0,0 +1,452 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: rabbitmq +// Service ID: 93 + +var _ resource.Resource = &RabbitmqResource{} +var _ resource.ResourceWithModifyPlan = &RabbitmqResource{} +var _ resource.ResourceWithImportState = &RabbitmqResource{} + +type RabbitmqResource struct { + client *core.UniversalClient +} + +type RabbitmqModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + IpSpaceNameMaster types.String `tfsdk:"ip_space_name_master"` + NeedExternalAddressMaster types.Bool `tfsdk:"need_external_address_master"` + ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"` + ResourceDisk types.Int64 `tfsdk:"resource_disk"` + ResourceInstances types.Int64 `tfsdk:"resource_instances"` + ResourceMemory types.Int64 `tfsdk:"resource_memory"` + ResourceRealm types.String `tfsdk:"resource_realm"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewRabbitmqResource() resource.Resource { + return &RabbitmqResource{} +} + +func (r *RabbitmqResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_rabbitmq" +} + +func (r *RabbitmqResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "ip_space_name_master": schema.StringAttribute{Optional: true, MarkdownDescription: "Пример: `internet-no-antiddos-v1`"}, + "need_external_address_master": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Флаг, определяющий, нужно ли выделять внешний IP для доступа к master. Пример: `true` или `false`"}, + "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1000), MarkdownDescription: "Пример: `2000`"}, + "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(30), MarkdownDescription: "Пример: `20`"}, + "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Пример: `1`"}, + "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1124), MarkdownDescription: "Пример: `2048`"}, + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8s-4-ext-nubes-ru"), MarkdownDescription: "Выбрать платформу из списка Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *RabbitmqResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *RabbitmqModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *RabbitmqModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan RabbitmqModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 116: resources_core.FormatInt64(config.ResourceCPU), + 117: resources_core.FormatInt64(config.ResourceMemory), + 118: resources_core.FormatInt64(config.ResourceDisk), + 119: resources_core.FormatString(config.ResourceRealm), + 224: resources_core.FormatInt64(config.ResourceInstances), + 225: resources_core.FormatBool(config.NeedExternalAddressMaster), + 543: resources_core.FormatString(config.IpSpaceNameMaster), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 93, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *RabbitmqResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data RabbitmqModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 93, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 116: resources_core.FormatInt64(data.ResourceCPU), + 117: resources_core.FormatInt64(data.ResourceMemory), + 118: resources_core.FormatInt64(data.ResourceDisk), + 119: resources_core.FormatString(data.ResourceRealm), + 224: resources_core.FormatInt64(data.ResourceInstances), + 225: resources_core.FormatBool(data.NeedExternalAddressMaster), + 543: resources_core.FormatString(data.IpSpaceNameMaster), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 93, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 93, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "ipSpaceNameMaster", Field: "IpSpaceNameMaster", Type: "string"}, + {Code: "needExternalAddressMaster", Field: "NeedExternalAddressMaster", Type: "bool"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *RabbitmqResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state RabbitmqModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 93, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "ipSpaceNameMaster", Field: "IpSpaceNameMaster", Type: "string"}, + {Code: "needExternalAddressMaster", Field: "NeedExternalAddressMaster", Type: "bool"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *RabbitmqResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan RabbitmqModel + var state RabbitmqModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.ResourceCPU.IsNull() != state.ResourceCPU.IsNull() || plan.ResourceCPU.IsUnknown() != state.ResourceCPU.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() { + if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceMemory.IsNull() != state.ResourceMemory.IsNull() || plan.ResourceMemory.IsUnknown() != state.ResourceMemory.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() { + if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceDisk.IsNull() != state.ResourceDisk.IsNull() || plan.ResourceDisk.IsUnknown() != state.ResourceDisk.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() { + if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceInstances.IsNull() != state.ResourceInstances.IsNull() || plan.ResourceInstances.IsUnknown() != state.ResourceInstances.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() { + if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.NeedExternalAddressMaster.IsNull() != state.NeedExternalAddressMaster.IsNull() || plan.NeedExternalAddressMaster.IsUnknown() != state.NeedExternalAddressMaster.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.NeedExternalAddressMaster.IsNull() && !plan.NeedExternalAddressMaster.IsUnknown() && !state.NeedExternalAddressMaster.IsNull() && !state.NeedExternalAddressMaster.IsUnknown() { + if plan.NeedExternalAddressMaster.ValueBool() != state.NeedExternalAddressMaster.ValueBool() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.IpSpaceNameMaster.IsNull() != state.IpSpaceNameMaster.IsNull() || plan.IpSpaceNameMaster.IsUnknown() != state.IpSpaceNameMaster.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.IpSpaceNameMaster.IsNull() && !plan.IpSpaceNameMaster.IsUnknown() && !state.IpSpaceNameMaster.IsNull() && !state.IpSpaceNameMaster.IsUnknown() { + if !strings.EqualFold(plan.IpSpaceNameMaster.ValueString(), state.IpSpaceNameMaster.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 304: resources_core.FormatInt64(plan.ResourceCPU), + 305: resources_core.FormatInt64(plan.ResourceMemory), + 306: resources_core.FormatInt64(plan.ResourceDisk), + 307: resources_core.FormatInt64(plan.ResourceInstances), + 308: resources_core.FormatBool(plan.NeedExternalAddressMaster), + 545: resources_core.FormatString(plan.IpSpaceNameMaster), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 93, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "ipSpaceNameMaster", Field: "IpSpaceNameMaster", Type: "string"}, + {Code: "needExternalAddressMaster", Field: "NeedExternalAddressMaster", Type: "bool"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *RabbitmqResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state RabbitmqModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *RabbitmqResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *RabbitmqResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/94_lucee_resource.go b/generated/prod/go/94_lucee_resource.go new file mode 100644 index 0000000..34033fe --- /dev/null +++ b/generated/prod/go/94_lucee_resource.go @@ -0,0 +1,449 @@ +package resources_gen + +import ( + "context" + "fmt" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: lucee +// Service ID: 94 + +var _ resource.Resource = &LuceeResource{} +var _ resource.ResourceWithModifyPlan = &LuceeResource{} +var _ resource.ResourceWithImportState = &LuceeResource{} + +type LuceeResource struct { + client *core.UniversalClient +} + +// LuceeAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration. +type LuceeAccessConfigurationModel struct { + Domain types.String `tfsdk:"domain" json:"domain"` +} + +// LuceeAppConfigurationModel — вложенная модель для map-fixed параметра appConfiguration. +type LuceeAppConfigurationModel struct { + Version types.String `tfsdk:"version" json:"version"` + GitPath types.String `tfsdk:"git_path" json:"gitPath"` + HealthPath types.String `tfsdk:"health_path" json:"healthPath"` +} + +// LuceeClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type LuceeClusterConfigurationModel struct { + Replicas types.Int64 `tfsdk:"replicas" json:"replicas"` + Cpu types.String `tfsdk:"cpu" json:"cpu"` + Memory types.Int64 `tfsdk:"memory" json:"memory"` +} + +// LuceeStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type LuceeStartupConfigurationModel struct { + ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"` +} + +type LuceeModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + // --- Redeploy support (ARCHITECTURE.md) --- + // git_revision: при изменении вызывает redeploy вместо modify. + // Опциональное поле — если не задано, modify работает как обычно. + GitRevision types.String `tfsdk:"git_revision"` + AccessConfiguration *LuceeAccessConfigurationModel `tfsdk:"access_configuration"` + AppConfiguration *LuceeAppConfigurationModel `tfsdk:"app_configuration"` + ClusterConfiguration *LuceeClusterConfigurationModel `tfsdk:"cluster_configuration"` + JsonEnv types.String `tfsdk:"json_env"` + StartupConfiguration *LuceeStartupConfigurationModel `tfsdk:"startup_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewLuceeResource() resource.Resource { + return &LuceeResource{} +} + +func (r *LuceeResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_lucee" +} + +func (r *LuceeResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("lucee-un0nzrwz"), MarkdownDescription: "Если указан FQDN в формате: `myapp.example.com`, предварительно будет проверяться DNS-запись (А-запись **должна смотреть на IP Ingress кластера ресурсной платформы**)

Если указан в формате `myapp`, DNS-запись будет автоматически создана в служебной DNS-зоне"}, + }}, + "app_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("6.0")}, + "git_path": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("https://github.com/xahys/testlucee"), MarkdownDescription: "Путь до репозитория приложения
Обязательно должен быть передан как URL: `https://....`

Должен быть сетевой доступ до URL для скачивания исходников"}, + "health_path": schema.StringAttribute{Optional: true, MarkdownDescription: "Путь до healtcheck для запуска пода в Kubernetes, который отдаст 200 код
Пустое значение означает, что будет проверяться исключительно доступность приложения по порту"}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)}, + "cpu": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("200"), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"}, + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(256), MarkdownDescription: "Указывается в Megabytes"}, + }}, + "json_env": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("{}"), MarkdownDescription: "Передаётся в формате КЛЮЧ:ЗНАЧЕНИЕ"}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8s-4-ext-nubes-ru"), MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"}, + }}, + // --- Redeploy support (ARCHITECTURE.md) --- + // При изменении git_revision вызывается redeploy вместо modify. + // Если не задано — modify работает без изменений. + "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *LuceeResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *LuceeModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *LuceeModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan LuceeModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 1019: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}), + 1020: resources_core.BuildJSON(map[string]string{"replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "cpu": fmt.Sprintf("\"%s\"", config.ClusterConfiguration.Cpu.ValueString()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64())}), + 1021: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", config.AccessConfiguration.Domain.ValueString())}), + 1022: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", config.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", config.AppConfiguration.HealthPath.ValueString())}), + 1023: resources_core.FormatString(config.JsonEnv), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 94, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *LuceeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data LuceeModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 94, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 1019: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}), + 1020: resources_core.BuildJSON(map[string]string{"replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "cpu": fmt.Sprintf("\"%s\"", data.ClusterConfiguration.Cpu.ValueString()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64())}), + 1021: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", data.AccessConfiguration.Domain.ValueString())}), + 1022: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", data.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", data.AppConfiguration.HealthPath.ValueString())}), + 1023: resources_core.FormatString(data.JsonEnv), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 94, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 94, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "jsonEnv", Field: "JsonEnv", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *LuceeResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state LuceeModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 94, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "jsonEnv", Field: "JsonEnv", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *LuceeResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan LuceeModel + var state LuceeModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.JsonEnv.IsNull() != state.JsonEnv.IsNull() || plan.JsonEnv.IsUnknown() != state.JsonEnv.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.JsonEnv.IsNull() && !plan.JsonEnv.IsUnknown() && !state.JsonEnv.IsNull() && !state.JsonEnv.IsUnknown() { + if !strings.EqualFold(plan.JsonEnv.ValueString(), state.JsonEnv.ValueString()) { + hasServiceParamChanges = true + } + } + } + // --- Redeploy support (ARCHITECTURE.md) --- + // Проверяем изменился ли git_revision. + // Если да — вызываем redeploy вместо modify. + // git_revision = "" (не задано) → обычный modify. + redeployRequested := false + if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() { + if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() { + redeployRequested = true + } + } + + if !hasServiceParamChanges && !redeployRequested { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + // --- Redeploy support (ARCHITECTURE.md) --- + if redeployRequested { + // Вызываем redeploy — пустой params (операция не требует CFS-параметров) + if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + } else { + params := map[int]string{ + 1024: resources_core.BuildJSON(map[string]string{"replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "cpu": fmt.Sprintf("\"%s\"", plan.ClusterConfiguration.Cpu.ValueString()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64())}), + 1025: resources_core.BuildJSON(map[string]string{"gitPath": fmt.Sprintf("\"%s\"", plan.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", plan.AppConfiguration.HealthPath.ValueString()), "version": fmt.Sprintf("\"%s\"", plan.AppConfiguration.Version.ValueString())}), + 1026: resources_core.FormatString(plan.JsonEnv), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 94, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "jsonEnv", Field: "JsonEnv", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *LuceeResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state LuceeModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *LuceeResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *LuceeResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/95_nodejs_resource.go b/generated/prod/go/95_nodejs_resource.go new file mode 100644 index 0000000..b9f5f92 --- /dev/null +++ b/generated/prod/go/95_nodejs_resource.go @@ -0,0 +1,449 @@ +package resources_gen + +import ( + "context" + "fmt" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: nodejs +// Service ID: 95 + +var _ resource.Resource = &NodejsResource{} +var _ resource.ResourceWithModifyPlan = &NodejsResource{} +var _ resource.ResourceWithImportState = &NodejsResource{} + +type NodejsResource struct { + client *core.UniversalClient +} + +// NodejsAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration. +type NodejsAccessConfigurationModel struct { + Domain types.String `tfsdk:"domain" json:"domain"` +} + +// NodejsAppConfigurationModel — вложенная модель для map-fixed параметра appConfiguration. +type NodejsAppConfigurationModel struct { + Version types.String `tfsdk:"version" json:"version"` + GitPath types.String `tfsdk:"git_path" json:"gitPath"` + HealthPath types.String `tfsdk:"health_path" json:"healthPath"` +} + +// NodejsClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type NodejsClusterConfigurationModel struct { + Cpu types.Int64 `tfsdk:"cpu" json:"cpu"` + Memory types.Int64 `tfsdk:"memory" json:"memory"` + Replicas types.Int64 `tfsdk:"replicas" json:"replicas"` +} + +// NodejsStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type NodejsStartupConfigurationModel struct { + ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"` +} + +type NodejsModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + // --- Redeploy support (ARCHITECTURE.md) --- + // git_revision: при изменении вызывает redeploy вместо modify. + // Опциональное поле — если не задано, modify работает как обычно. + GitRevision types.String `tfsdk:"git_revision"` + AccessConfiguration *NodejsAccessConfigurationModel `tfsdk:"access_configuration"` + AppConfiguration *NodejsAppConfigurationModel `tfsdk:"app_configuration"` + ClusterConfiguration *NodejsClusterConfigurationModel `tfsdk:"cluster_configuration"` + JsonEnv types.String `tfsdk:"json_env"` + StartupConfiguration *NodejsStartupConfigurationModel `tfsdk:"startup_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewNodejsResource() resource.Resource { + return &NodejsResource{} +} + +func (r *NodejsResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_nodejs" +} + +func (r *NodejsResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("node-mt4rik7o"), MarkdownDescription: "Если указан FQDN в формате: `myapp.example.com`, предварительно будет проверяться DNS-запись (А-запись **должна смотреть на IP Ingress кластера ресурсной платформы**)

Если указан в формате `myapp`, DNS-запись будет автоматически создана в служебной DNS-зоне"}, + }}, + "app_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("23")}, + "git_path": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("https://github.com/xahys/testnode.git"), MarkdownDescription: "Путь до репозитория приложения
Обязательно должен быть передан как URL: `https://....`

Должен быть сетевой доступ до URL для скачивания исходников"}, + "health_path": schema.StringAttribute{Optional: true, MarkdownDescription: "Путь до healtcheck для запуска пода в Kubernetes, который отдаст 200 код
Пустое значение означает, что будет проверяться исключительно доступность приложения по порту"}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(200), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"}, + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(256), MarkdownDescription: "Указывается в Megabytes"}, + "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)}, + }}, + "json_env": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("{}"), MarkdownDescription: "Передаётся в формате КЛЮЧ:ЗНАЧЕНИЕ"}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8s-4-ext-nubes-ru"), MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"}, + }}, + // --- Redeploy support (ARCHITECTURE.md) --- + // При изменении git_revision вызывается redeploy вместо modify. + // Если не задано — modify работает без изменений. + "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *NodejsResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *NodejsModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *NodejsModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan NodejsModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 1028: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}), + 1029: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64())}), + 1030: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", config.AccessConfiguration.Domain.ValueString())}), + 1031: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", config.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", config.AppConfiguration.HealthPath.ValueString())}), + 1032: resources_core.FormatString(config.JsonEnv), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 95, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *NodejsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data NodejsModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 95, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 1028: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}), + 1029: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64())}), + 1030: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", data.AccessConfiguration.Domain.ValueString())}), + 1031: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", data.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", data.AppConfiguration.HealthPath.ValueString())}), + 1032: resources_core.FormatString(data.JsonEnv), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 95, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 95, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "jsonEnv", Field: "JsonEnv", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *NodejsResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state NodejsModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 95, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "jsonEnv", Field: "JsonEnv", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *NodejsResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan NodejsModel + var state NodejsModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.JsonEnv.IsNull() != state.JsonEnv.IsNull() || plan.JsonEnv.IsUnknown() != state.JsonEnv.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.JsonEnv.IsNull() && !plan.JsonEnv.IsUnknown() && !state.JsonEnv.IsNull() && !state.JsonEnv.IsUnknown() { + if !strings.EqualFold(plan.JsonEnv.ValueString(), state.JsonEnv.ValueString()) { + hasServiceParamChanges = true + } + } + } + // --- Redeploy support (ARCHITECTURE.md) --- + // Проверяем изменился ли git_revision. + // Если да — вызываем redeploy вместо modify. + // git_revision = "" (не задано) → обычный modify. + redeployRequested := false + if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() { + if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() { + redeployRequested = true + } + } + + if !hasServiceParamChanges && !redeployRequested { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + // --- Redeploy support (ARCHITECTURE.md) --- + if redeployRequested { + // Вызываем redeploy — пустой params (операция не требует CFS-параметров) + if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + } else { + params := map[int]string{ + 1033: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64())}), + 1034: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", plan.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", plan.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", plan.AppConfiguration.HealthPath.ValueString())}), + 1035: resources_core.FormatString(plan.JsonEnv), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 95, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "jsonEnv", Field: "JsonEnv", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *NodejsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state NodejsModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *NodejsResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *NodejsResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/96_pgadmin_resource.go b/generated/prod/go/96_pgadmin_resource.go new file mode 100644 index 0000000..7d7568f --- /dev/null +++ b/generated/prod/go/96_pgadmin_resource.go @@ -0,0 +1,504 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: pgadmin +// Service ID: 96 + +var _ resource.Resource = &PgadminResource{} +var _ resource.ResourceWithModifyPlan = &PgadminResource{} +var _ resource.ResourceWithImportState = &PgadminResource{} + +type PgadminResource struct { + client *core.UniversalClient +} + +type PgadminModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + // --- Redeploy support (ARCHITECTURE.md) --- + // git_revision: при изменении вызывает redeploy вместо modify. + // Опциональное поле — если не задано, modify работает как обычно. + GitRevision types.String `tfsdk:"git_revision"` + Domain types.String `tfsdk:"domain"` + Login types.String `tfsdk:"login"` + Password types.String `tfsdk:"password"` + ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"` + ResourceDisk types.Int64 `tfsdk:"resource_disk"` + ResourceMemory types.Int64 `tfsdk:"resource_memory"` + ResourceRealm types.String `tfsdk:"resource_realm"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewPgadminResource() resource.Resource { + return &PgadminResource{} +} + +func (r *PgadminResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_pgadmin" +} + +func (r *PgadminResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("pgadmin-1"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."}, + "login": schema.StringAttribute{Required: true, MarkdownDescription: "Логин для подключения к pgAdmin, в формате email (login@email.net) Не изменяется после создания."}, + "password": schema.StringAttribute{Required: true, MarkdownDescription: "Пароль для подключения к pgAdmin Не изменяется после создания.", Sensitive: true}, + "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Квота ядра пода (milicores). 1000Mili = 1 Ядро"}, + "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Квота диска (Указывается в GIGAbytes)"}, + "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Квота памяти пода (MBytes)"}, + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8s-4-ext-nubes-ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + // --- Redeploy support (ARCHITECTURE.md) --- + // При изменении git_revision вызывается redeploy вместо modify. + // Если не задано — modify работает без изменений. + "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *PgadminResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *PgadminModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *PgadminModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan PgadminModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.Login.IsNull() && !plan.Login.IsUnknown() && !state.Login.IsNull() && !state.Login.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Login.ValueString(), state.Login.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить login", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.Password.IsNull() && !plan.Password.IsUnknown() && !state.Password.IsNull() && !state.Password.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Password.ValueString(), state.Password.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить password", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.Login.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "login is required.") + return + } + if config.Login.IsUnknown() { + return + } + if config.Password.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "password is required.") + return + } + if config.Password.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 164: resources_core.FormatString(config.Domain), + 165: resources_core.FormatInt64(config.ResourceCPU), + 166: resources_core.FormatInt64(config.ResourceMemory), + 167: resources_core.FormatInt64(config.ResourceDisk), + 169: resources_core.FormatString(config.ResourceRealm), + 170: resources_core.FormatString(config.Login), + 171: resources_core.FormatString(config.Password), + } + desiredDomain := "" + if !config.Domain.IsNull() && !config.Domain.IsUnknown() { + desiredDomain = config.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 96, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *PgadminResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data PgadminModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.Login.IsNull() || data.Login.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "login is required.") + return + } + if data.Password.IsNull() || data.Password.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "password is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + if !data.Domain.IsNull() && !data.Domain.IsUnknown() { + desiredDomain = data.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 96, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 164: resources_core.FormatString(data.Domain), + 165: resources_core.FormatInt64(data.ResourceCPU), + 166: resources_core.FormatInt64(data.ResourceMemory), + 167: resources_core.FormatInt64(data.ResourceDisk), + 169: resources_core.FormatString(data.ResourceRealm), + 170: resources_core.FormatString(data.Login), + 171: resources_core.FormatString(data.Password), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 96, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 96, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "login", Field: "Login", Type: "string"}, + {Code: "password", Field: "Password", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *PgadminResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state PgadminModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 96, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "login", Field: "Login", Type: "string"}, + {Code: "password", Field: "Password", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *PgadminResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan PgadminModel + var state PgadminModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.ResourceCPU.IsNull() != state.ResourceCPU.IsNull() || plan.ResourceCPU.IsUnknown() != state.ResourceCPU.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() { + if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceMemory.IsNull() != state.ResourceMemory.IsNull() || plan.ResourceMemory.IsUnknown() != state.ResourceMemory.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() { + if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceDisk.IsNull() != state.ResourceDisk.IsNull() || plan.ResourceDisk.IsUnknown() != state.ResourceDisk.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() { + if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() { + hasServiceParamChanges = true + } + } + } + // --- Redeploy support (ARCHITECTURE.md) --- + // Проверяем изменился ли git_revision. + // Если да — вызываем redeploy вместо modify. + // git_revision = "" (не задано) → обычный modify. + redeployRequested := false + if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() { + if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() { + redeployRequested = true + } + } + + if !hasServiceParamChanges && !redeployRequested { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + // --- Redeploy support (ARCHITECTURE.md) --- + if redeployRequested { + // Вызываем redeploy — пустой params (операция не требует CFS-параметров) + if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + } else { + params := map[int]string{ + 172: resources_core.FormatInt64(plan.ResourceCPU), + 173: resources_core.FormatInt64(plan.ResourceMemory), + 174: resources_core.FormatInt64(plan.ResourceDisk), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 96, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "login", Field: "Login", Type: "string"}, + {Code: "password", Field: "Password", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *PgadminResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state PgadminModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *PgadminResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *PgadminResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/97_nodered_resource.go b/generated/prod/go/97_nodered_resource.go new file mode 100644 index 0000000..147bc59 --- /dev/null +++ b/generated/prod/go/97_nodered_resource.go @@ -0,0 +1,420 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: nodered +// Service ID: 97 + +var _ resource.Resource = &NoderedResource{} +var _ resource.ResourceWithModifyPlan = &NoderedResource{} +var _ resource.ResourceWithImportState = &NoderedResource{} + +type NoderedResource struct { + client *core.UniversalClient +} + +type NoderedModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + Domain types.String `tfsdk:"domain"` + ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"` + ResourceDisk types.Int64 `tfsdk:"resource_disk"` + ResourceInstances types.Int64 `tfsdk:"resource_instances"` + ResourceMemory types.Int64 `tfsdk:"resource_memory"` + ResourceRealm types.String `tfsdk:"resource_realm"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewNoderedResource() resource.Resource { + return &NoderedResource{} +} + +func (r *NoderedResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_nodered" +} + +func (r *NoderedResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("nodered-1"), MarkdownDescription: "Наименование поддомена, будет добавлено в k8stemplate.dev.nubes.ru Не изменяется после создания."}, + "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Квота ядра пода (Указывается в mili). 1000Mili = 1 Ядро Не изменяется после создания."}, + "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Квота диска (Указывается в GIGAbytes) Не изменяется после создания."}, + "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Количество узлов Не изменяется после создания."}, + "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Квота памяти пода (Указывается в Megabytes) Не изменяется после создания."}, + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8s-4-ext-nubes-ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *NoderedResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *NoderedModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *NoderedModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan NoderedModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() { + if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_c_p_u", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() { + if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_disk", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() { + if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_instances", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() { + if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_memory", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 193: resources_core.FormatInt64(config.ResourceCPU), + 194: resources_core.FormatInt64(config.ResourceMemory), + 195: resources_core.FormatInt64(config.ResourceDisk), + 196: resources_core.FormatString(config.ResourceRealm), + 197: resources_core.FormatString(config.Domain), + 323: resources_core.FormatInt64(config.ResourceInstances), + } + desiredDomain := "" + if !config.Domain.IsNull() && !config.Domain.IsUnknown() { + desiredDomain = config.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 97, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *NoderedResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data NoderedModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + if !data.Domain.IsNull() && !data.Domain.IsUnknown() { + desiredDomain = data.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 97, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 193: resources_core.FormatInt64(data.ResourceCPU), + 194: resources_core.FormatInt64(data.ResourceMemory), + 195: resources_core.FormatInt64(data.ResourceDisk), + 196: resources_core.FormatString(data.ResourceRealm), + 197: resources_core.FormatString(data.Domain), + 323: resources_core.FormatInt64(data.ResourceInstances), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 97, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 97, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *NoderedResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state NoderedModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 97, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *NoderedResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan NoderedModel + var state NoderedModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 97, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *NoderedResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state NoderedModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *NoderedResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *NoderedResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/98_http_resource.go b/generated/prod/go/98_http_resource.go new file mode 100644 index 0000000..1965e6d --- /dev/null +++ b/generated/prod/go/98_http_resource.go @@ -0,0 +1,498 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: http +// Service ID: 98 + +var _ resource.Resource = &HttpResource{} +var _ resource.ResourceWithModifyPlan = &HttpResource{} +var _ resource.ResourceWithImportState = &HttpResource{} + +type HttpResource struct { + client *core.UniversalClient +} + +type HttpModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + // --- Redeploy support (ARCHITECTURE.md) --- + // git_revision: при изменении вызывает redeploy вместо modify. + // Опциональное поле — если не задано, modify работает как обычно. + GitRevision types.String `tfsdk:"git_revision"` + Domain types.String `tfsdk:"domain"` + IngressAnnotations types.String `tfsdk:"ingress_annotations"` + JsonEnv types.String `tfsdk:"json_env"` + RegistryPath types.String `tfsdk:"registry_path"` + ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"` + ResourceInstances types.Int64 `tfsdk:"resource_instances"` + ResourceMemory types.Int64 `tfsdk:"resource_memory"` + ResourceRealm types.String `tfsdk:"resource_realm"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewHttpResource() resource.Resource { + return &HttpResource{} +} + +func (r *HttpResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_http" +} + +func (r *HttpResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("http-1"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."}, + "ingress_annotations": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("{}"), MarkdownDescription: "Формат ключ/значение
Поддерживает пока простые аннотации Не изменяется после создания."}, + "json_env": schema.StringAttribute{Optional: true, MarkdownDescription: "Массив переменных, будут переданы в env приложения Не изменяется после создания."}, + "registry_path": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("nexus-sa.tst.nubes.ru/docker-nubes/jolt"), MarkdownDescription: "Путь до image, который будет установлен как deployment Не изменяется после создания."}, + "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100), MarkdownDescription: "Квота ядра пода (Указывается в mili)"}, + "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Количество узлов"}, + "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(200), MarkdownDescription: "Квота памяти пода (Указывается в Megabytes)"}, + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8s-4-ext-nubes-ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + // --- Redeploy support (ARCHITECTURE.md) --- + // При изменении git_revision вызывается redeploy вместо modify. + // Если не задано — modify работает без изменений. + "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *HttpResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *HttpModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *HttpModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan HttpModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.IngressAnnotations.IsNull() && !plan.IngressAnnotations.IsUnknown() && !state.IngressAnnotations.IsNull() && !state.IngressAnnotations.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.IngressAnnotations.ValueString(), state.IngressAnnotations.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить ingress_annotations", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.JsonEnv.IsNull() && !plan.JsonEnv.IsUnknown() && !state.JsonEnv.IsNull() && !state.JsonEnv.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.JsonEnv.ValueString(), state.JsonEnv.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить json_env", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.RegistryPath.IsNull() && !plan.RegistryPath.IsUnknown() && !state.RegistryPath.IsNull() && !state.RegistryPath.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.RegistryPath.ValueString(), state.RegistryPath.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить registry_path", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 212: resources_core.FormatString(config.Domain), + 213: resources_core.FormatInt64(config.ResourceCPU), + 214: resources_core.FormatInt64(config.ResourceMemory), + 215: resources_core.FormatInt64(config.ResourceInstances), + 216: resources_core.FormatString(config.ResourceRealm), + 217: resources_core.FormatString(config.RegistryPath), + 241: resources_core.FormatString(config.JsonEnv), + 782: resources_core.FormatString(config.IngressAnnotations), + } + desiredDomain := "" + if !config.Domain.IsNull() && !config.Domain.IsUnknown() { + desiredDomain = config.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 98, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *HttpResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data HttpModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + if !data.Domain.IsNull() && !data.Domain.IsUnknown() { + desiredDomain = data.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 98, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 212: resources_core.FormatString(data.Domain), + 213: resources_core.FormatInt64(data.ResourceCPU), + 214: resources_core.FormatInt64(data.ResourceMemory), + 215: resources_core.FormatInt64(data.ResourceInstances), + 216: resources_core.FormatString(data.ResourceRealm), + 217: resources_core.FormatString(data.RegistryPath), + 241: resources_core.FormatString(data.JsonEnv), + 782: resources_core.FormatString(data.IngressAnnotations), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 98, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 98, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "ingressAnnotations", Field: "IngressAnnotations", Type: "string"}, + {Code: "jsonEnv", Field: "JsonEnv", Type: "string"}, + {Code: "registryPath", Field: "RegistryPath", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *HttpResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state HttpModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 98, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "ingressAnnotations", Field: "IngressAnnotations", Type: "string"}, + {Code: "jsonEnv", Field: "JsonEnv", Type: "string"}, + {Code: "registryPath", Field: "RegistryPath", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *HttpResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan HttpModel + var state HttpModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.ResourceCPU.IsNull() != state.ResourceCPU.IsNull() || plan.ResourceCPU.IsUnknown() != state.ResourceCPU.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() { + if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceMemory.IsNull() != state.ResourceMemory.IsNull() || plan.ResourceMemory.IsUnknown() != state.ResourceMemory.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() { + if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceInstances.IsNull() != state.ResourceInstances.IsNull() || plan.ResourceInstances.IsUnknown() != state.ResourceInstances.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() { + if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() { + hasServiceParamChanges = true + } + } + } + // --- Redeploy support (ARCHITECTURE.md) --- + // Проверяем изменился ли git_revision. + // Если да — вызываем redeploy вместо modify. + // git_revision = "" (не задано) → обычный modify. + redeployRequested := false + if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() { + if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() { + redeployRequested = true + } + } + + if !hasServiceParamChanges && !redeployRequested { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + // --- Redeploy support (ARCHITECTURE.md) --- + if redeployRequested { + // Вызываем redeploy — пустой params (операция не требует CFS-параметров) + if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + } else { + params := map[int]string{ + 218: resources_core.FormatInt64(plan.ResourceCPU), + 219: resources_core.FormatInt64(plan.ResourceMemory), + 220: resources_core.FormatInt64(plan.ResourceInstances), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 98, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "ingressAnnotations", Field: "IngressAnnotations", Type: "string"}, + {Code: "jsonEnv", Field: "JsonEnv", Type: "string"}, + {Code: "registryPath", Field: "RegistryPath", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *HttpResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state HttpModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *HttpResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *HttpResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/99_gitea_resource.go b/generated/prod/go/99_gitea_resource.go new file mode 100644 index 0000000..ca019ef --- /dev/null +++ b/generated/prod/go/99_gitea_resource.go @@ -0,0 +1,579 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: gitea +// Service ID: 99 + +var _ resource.Resource = &GiteaResource{} +var _ resource.ResourceWithModifyPlan = &GiteaResource{} +var _ resource.ResourceWithImportState = &GiteaResource{} + +type GiteaResource struct { + client *core.UniversalClient +} + +type GiteaModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + Domain types.String `tfsdk:"domain"` + IngressExtraAnnotations types.String `tfsdk:"ingress_extra_annotations"` + IniServerParams types.String `tfsdk:"ini_server_params"` + PsqlUid types.String `tfsdk:"psql_uid"` + ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"` + ResourceDisk types.Int64 `tfsdk:"resource_disk"` + ResourceInstances types.Int64 `tfsdk:"resource_instances"` + ResourceMemory types.Int64 `tfsdk:"resource_memory"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewGiteaResource() resource.Resource { + return &GiteaResource{} +} + +func (r *GiteaResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_gitea" +} + +func (r *GiteaResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("gitea-1"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."}, + "ingress_extra_annotations": schema.StringAttribute{Optional: true}, + "ini_server_params": schema.StringAttribute{Optional: true}, + "psql_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UUID Услуги k8sPgsql. Пользователя techGitea, базу данных gitea создаёт автоматически Не изменяется после создания."}, + "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Квота ядра пода (Указывается в mili). 1000Mili = 1 Ядро"}, + "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Квота диска (Указывается в GIGAbytes)"}, + "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Количество узлов"}, + "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Квота памяти пода (Указывается в Megabytes)"}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *GiteaResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *GiteaModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *GiteaModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan GiteaModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan. + // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов. + // Resolve (displayName → UUID или uppercase → lowercase) нужен только для + // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка + // "Provider produced invalid plan: planned value does not match config value". + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.PsqlUid.IsNull() && !plan.PsqlUid.IsUnknown() && !state.PsqlUid.IsNull() && !state.PsqlUid.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.PsqlUid.ValueString(), state.PsqlUid.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить psql_uid", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.PsqlUid.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "psql_uid is required.") + return + } + if config.PsqlUid.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + if !config.PsqlUid.IsNull() && !config.PsqlUid.IsUnknown() { + resolvedPsqlUid, err := r.client.ResolveRefSvcParamValue(ctx, 90, config.PsqlUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve psql_uid", err.Error()) + } else if resolvedPsqlUid != "" && resolvedPsqlUid != config.PsqlUid.ValueString() { + config.PsqlUid = types.StringValue(resolvedPsqlUid) + } + } + params := map[int]string{ + 232: resources_core.FormatInt64(config.ResourceCPU), + 233: resources_core.FormatInt64(config.ResourceMemory), + 234: resources_core.FormatInt64(config.ResourceDisk), + 235: resources_core.FormatInt64(config.ResourceInstances), + 237: resources_core.FormatString(config.Domain), + 238: resources_core.FormatString(config.PsqlUid), + 818: resources_core.FormatString(config.IngressExtraAnnotations), + 821: resources_core.FormatString(config.IniServerParams), + } + desiredDomain := "" + if !config.Domain.IsNull() && !config.Domain.IsUnknown() { + desiredDomain = config.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 99, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *GiteaResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data GiteaModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.PsqlUid.IsNull() || data.PsqlUid.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "psql_uid is required.") + return + } + // ══════════════════════════════════════════════════════════════════════════ + // ПРАВИЛО TERRAFORM (официальная документация): + // «If an attribute value is configured, it is NEVER valid to change that + // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что + // написал пользователь). Менять plan запрещено на уровне фреймворка. + // + // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре. + // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный). + // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит: + // «Provider produced inconsistent result after apply». + // + // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот. + // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как + // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API). + // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный + // регистр в state через strings.EqualFold (сравниваем без учёта регистра). + // ══════════════════════════════════════════════════════════════════════════ + originalPsqlUid := data.PsqlUid + if !data.PsqlUid.IsNull() && !data.PsqlUid.IsUnknown() { + resolvedPsqlUid, err := r.client.ResolveRefSvcParamValue(ctx, 90, data.PsqlUid.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if resolvedPsqlUid != "" && resolvedPsqlUid != data.PsqlUid.ValueString() { + data.PsqlUid = types.StringValue(resolvedPsqlUid) + } + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + if !data.Domain.IsNull() && !data.Domain.IsUnknown() { + desiredDomain = data.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 99, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 232: resources_core.FormatInt64(data.ResourceCPU), + 233: resources_core.FormatInt64(data.ResourceMemory), + 234: resources_core.FormatInt64(data.ResourceDisk), + 235: resources_core.FormatInt64(data.ResourceInstances), + 237: resources_core.FormatString(data.Domain), + 238: resources_core.FormatString(data.PsqlUid), + 818: resources_core.FormatString(data.IngressExtraAnnotations), + 821: resources_core.FormatString(data.IniServerParams), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 99, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 99, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "ingressExtraAnnotations", Field: "IngressExtraAnnotations", Type: "string"}, + {Code: "iniServerParams", Field: "IniServerParams", Type: "string"}, + {Code: "psqlUid", Field: "PsqlUid", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.PsqlUid.IsNull() && !state.PsqlUid.IsUnknown() { + resolvedPsqlUid, err := r.client.ResolveRefSvcParamValue(ctx, 90, state.PsqlUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve psql_uid", err.Error()) + } else if resolvedPsqlUid != "" && resolvedPsqlUid != state.PsqlUid.ValueString() { + state.PsqlUid = types.StringValue(resolvedPsqlUid) + } + } + // Restore user-provided casing в state (Create). + // EqualFold = «совпадают ли значения без учёта регистра?» + // Если да — значит API вернул «туже» строку, только в другом регистре. + // Заменяем state на original (то что было в plan/config пользователя). + // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint. + if !originalPsqlUid.IsNull() && !originalPsqlUid.IsUnknown() && !state.PsqlUid.IsNull() && !state.PsqlUid.IsUnknown() { + if strings.EqualFold(state.PsqlUid.ValueString(), originalPsqlUid.ValueString()) { + state.PsqlUid = originalPsqlUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *GiteaResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state GiteaModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 99, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "ingressExtraAnnotations", Field: "IngressExtraAnnotations", Type: "string"}, + {Code: "iniServerParams", Field: "IniServerParams", Type: "string"}, + {Code: "psqlUid", Field: "PsqlUid", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !newState.PsqlUid.IsNull() && !newState.PsqlUid.IsUnknown() { + resolvedPsqlUid, err := r.client.ResolveRefSvcParamValue(ctx, 90, newState.PsqlUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve psql_uid", err.Error()) + } else if resolvedPsqlUid != "" && resolvedPsqlUid != newState.PsqlUid.ValueString() { + newState.PsqlUid = types.StringValue(resolvedPsqlUid) + } + } + // Restore user-provided casing в state (Read). + // При чтении у нас нет plan — но предыдущий state уже хранит значение + // в регистре пользователя (после первого Create оно было восстановлено). + // Берём prior state (переменная state) как эталон регистра. + // Если API вернул то же UUID только строчными буквами — восстанавливаем. + if !state.PsqlUid.IsNull() && !state.PsqlUid.IsUnknown() && !newState.PsqlUid.IsNull() && !newState.PsqlUid.IsUnknown() { + if strings.EqualFold(newState.PsqlUid.ValueString(), state.PsqlUid.ValueString()) { + newState.PsqlUid = state.PsqlUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *GiteaResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan GiteaModel + var state GiteaModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.ResourceCPU.IsNull() != state.ResourceCPU.IsNull() || plan.ResourceCPU.IsUnknown() != state.ResourceCPU.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() { + if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceMemory.IsNull() != state.ResourceMemory.IsNull() || plan.ResourceMemory.IsUnknown() != state.ResourceMemory.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() { + if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceDisk.IsNull() != state.ResourceDisk.IsNull() || plan.ResourceDisk.IsUnknown() != state.ResourceDisk.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() { + if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceInstances.IsNull() != state.ResourceInstances.IsNull() || plan.ResourceInstances.IsUnknown() != state.ResourceInstances.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() { + if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.IngressExtraAnnotations.IsNull() != state.IngressExtraAnnotations.IsNull() || plan.IngressExtraAnnotations.IsUnknown() != state.IngressExtraAnnotations.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.IngressExtraAnnotations.IsNull() && !plan.IngressExtraAnnotations.IsUnknown() && !state.IngressExtraAnnotations.IsNull() && !state.IngressExtraAnnotations.IsUnknown() { + if !strings.EqualFold(plan.IngressExtraAnnotations.ValueString(), state.IngressExtraAnnotations.ValueString()) { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.IniServerParams.IsNull() != state.IniServerParams.IsNull() || plan.IniServerParams.IsUnknown() != state.IniServerParams.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.IniServerParams.IsNull() && !plan.IniServerParams.IsUnknown() && !state.IniServerParams.IsNull() && !state.IniServerParams.IsUnknown() { + if !strings.EqualFold(plan.IniServerParams.ValueString(), state.IniServerParams.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 259: resources_core.FormatInt64(plan.ResourceCPU), + 260: resources_core.FormatInt64(plan.ResourceMemory), + 261: resources_core.FormatInt64(plan.ResourceDisk), + 262: resources_core.FormatInt64(plan.ResourceInstances), + 819: resources_core.FormatString(plan.IngressExtraAnnotations), + 820: resources_core.FormatString(plan.IniServerParams), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 99, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "ingressExtraAnnotations", Field: "IngressExtraAnnotations", Type: "string"}, + {Code: "iniServerParams", Field: "IniServerParams", Type: "string"}, + {Code: "psqlUid", Field: "PsqlUid", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.PsqlUid.IsNull() && !state.PsqlUid.IsUnknown() { + resolvedPsqlUid, err := r.client.ResolveRefSvcParamValue(ctx, 90, state.PsqlUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve psql_uid", err.Error()) + } else if resolvedPsqlUid != "" && resolvedPsqlUid != state.PsqlUid.ValueString() { + state.PsqlUid = types.StringValue(resolvedPsqlUid) + } + } + // Restore user-provided casing в state (Update). + // После API-вызова modify state содержит значения в lower-case от API. + // Plan == config == то что написал пользователь (регистр неизменён). + // EqualFold: если UUID совпадает без учёта регистра — берём из plan. + if !plan.PsqlUid.IsNull() && !plan.PsqlUid.IsUnknown() && !state.PsqlUid.IsNull() && !state.PsqlUid.IsUnknown() { + if strings.EqualFold(state.PsqlUid.ValueString(), plan.PsqlUid.ValueString()) { + state.PsqlUid = plan.PsqlUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *GiteaResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state GiteaModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *GiteaResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *GiteaResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/prod/go/registry.go b/generated/prod/go/registry.go new file mode 100644 index 0000000..de4c11b --- /dev/null +++ b/generated/prod/go/registry.go @@ -0,0 +1,73 @@ +package resources_gen + +import "github.com/hashicorp/terraform-plugin-framework/resource" + +// Code generated by tools/gen_v2. DO NOT EDIT. +func AllResources() []func() resource.Resource { + return []func() resource.Resource{ + NewAkhqResource, + NewClickhouseResource, + NewDnsrecordResource, + NewDnszoneResource, + NewDummyResource, + NewFlaskResource, + NewGiteaResource, + NewGiteaComplexResource, + NewHarborResource, + NewHttpResource, + NewK8sSthutrvalClusterResource, + NewK8sVeleroResource, + NewK8sZitiControllerResource, + NewKafkaResource, + NewLlmAiResource, + NewLuceeResource, + NewMariadbResource, + NewMongodbResource, + NewNextcloudResource, + NewNifiResource, + NewNodejsResource, + NewNoderedResource, + NewOpenwhiskResource, + NewPgadminResource, + NewPostgresResource, + NewRabbitmqResource, + NewRedisResource, + NewS3Resource, + NewS3bucketResource, + NewSupersetResource, + NewTemplateResource, + NewTenantResource, + NewValoTenantResource, + NewVappResource, + NewVcComplexResource, + NewVcMgmtSthutrvalClusterResource, + NewVcNsxtResource, + NewVcOrgResource, + NewVcOrgSaasResource, + NewVcVdcResource, + NewVcVdcGroupResource, + NewVcVmResource, + NewVcVmV2Resource, + NewVcVmV3Resource, + NewVcexternalipResource, + NewZonesV2Resource, + NewClickhouseDatabaseResource, + NewClickhouseUserResource, + NewK8sSthutrvalClusterUserResource, + NewKafkaTopicResource, + NewKafkaUserResource, + NewLlmAiSecretsResource, + NewLlmAiTeamResource, + NewMariadbDatabaseResource, + NewMariadbUserResource, + NewMongodbUserResource, + NewNextcloudBackupResource, + NewPostgresBackupResource, + NewPostgresDatabaseResource, + NewPostgresUserResource, + NewS3SubUserResource, + NewVcOrgUserResource, + NewVcVdcGroupVdcResource, + NewZonesV2RecordResource, + } +} diff --git a/generated/test/go/100_openwhisk_resource.go b/generated/test/go/100_openwhisk_resource.go new file mode 100644 index 0000000..3c65789 --- /dev/null +++ b/generated/test/go/100_openwhisk_resource.go @@ -0,0 +1,322 @@ +package resources_gen + +import ( + "context" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: openwhisk +// Service ID: 100 + +var _ resource.Resource = &OpenwhiskResource{} +var _ resource.ResourceWithModifyPlan = &OpenwhiskResource{} +var _ resource.ResourceWithImportState = &OpenwhiskResource{} + +type OpenwhiskResource struct { + client *core.UniversalClient +} + +type OpenwhiskModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewOpenwhiskResource() resource.Resource { + return &OpenwhiskResource{} +} + +func (r *OpenwhiskResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_openwhisk" +} + +func (r *OpenwhiskResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *OpenwhiskResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *OpenwhiskModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *OpenwhiskModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan OpenwhiskModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{} + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 100, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *OpenwhiskResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data OpenwhiskModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 100, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{} + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 100, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 100, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{}) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *OpenwhiskResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state OpenwhiskModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 100, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{}) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *OpenwhiskResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan OpenwhiskModel + var state OpenwhiskModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 100, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{}) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *OpenwhiskResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state OpenwhiskModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *OpenwhiskResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *OpenwhiskResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/109_zones_v2_record_resource.go b/generated/test/go/109_zones_v2_record_resource.go new file mode 100644 index 0000000..b875e09 --- /dev/null +++ b/generated/test/go/109_zones_v2_record_resource.go @@ -0,0 +1,366 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: zones_v2 +// Subresource: record + +var _ resource.Resource = &ZonesV2RecordResource{} + +// ZonesV2RecordResource manages a subresource via operations. +type ZonesV2RecordResource struct { + client *core.UniversalClient +} + +type ZonesV2RecordModel struct { + ID types.String `tfsdk:"id"` + ZonesV2ID types.String `tfsdk:"zones_v2_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + RecordInput types.String `tfsdk:"record_input"` + RecordName types.String `tfsdk:"record_name"` + RecordTTL types.Int64 `tfsdk:"record_t_t_l"` + RecordType types.String `tfsdk:"record_type"` +} + +func NewZonesV2RecordResource() resource.Resource { + return &ZonesV2RecordResource{} +} + +func (r *ZonesV2RecordResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_zones_v2_record" +} + +func (r *ZonesV2RecordResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "zones_v2_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "record_input": schema.StringAttribute{Required: true, MarkdownDescription: "Валидация производится только после запуска операции. Если при указанном типе будет указано некорректно значение, это будет описано в логе ошибки операции"}, + "record_name": schema.StringAttribute{Required: true, MarkdownDescription: "Имя зоны в конце проставляется автоматически Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "record_t_t_l": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(3600), MarkdownDescription: "Time-to-Leave записи. Раз в какое время запись будет обновлена"}, + "record_type": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("A")}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *ZonesV2RecordResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan ZonesV2RecordModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if plan.RecordInput.IsNull() || plan.RecordInput.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "record_input is required.") + return + } + if plan.RecordName.IsNull() || plan.RecordName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "record_name is required.") + return + } + + instanceUID := strings.TrimSpace(plan.ZonesV2ID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "recordName": resources_core.FormatString(plan.RecordName), + } + identityCodes := []string{ + "recordName", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("record", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "record", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "recordName": resources_core.FormatString(plan.RecordName), + "recordTTL": resources_core.FormatInt64(plan.RecordTTL), + "recordType": resources_core.FormatString(plan.RecordType), + "recordInput": resources_core.FormatString(plan.RecordInput), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_record", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_record", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "record", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "record", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "record", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *ZonesV2RecordResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state ZonesV2RecordModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *ZonesV2RecordResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan ZonesV2RecordModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "modify_record" == "" { + var state ZonesV2RecordModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.ZonesV2ID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{ + "recordName": resources_core.FormatString(plan.RecordName), + "recordTTL": resources_core.FormatInt64(plan.RecordTTL), + "recordType": resources_core.FormatString(plan.RecordType), + "recordInput": resources_core.FormatString(plan.RecordInput), + }) + + idParams := map[string]string{ + "recordName": resources_core.FormatString(plan.RecordName), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "modify_record", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "record", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *ZonesV2RecordResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state ZonesV2RecordModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_record" == "" { + return + } + + instanceUID := strings.TrimSpace(state.ZonesV2ID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "recordName": resources_core.FormatString(state.RecordName), + } + identityCodes := []string{ + "recordName", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("record", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "recordName": resources_core.FormatString(state.RecordName), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_record", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *ZonesV2RecordResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/109_zones_v2_resource.go b/generated/test/go/109_zones_v2_resource.go new file mode 100644 index 0000000..52dbc10 --- /dev/null +++ b/generated/test/go/109_zones_v2_resource.go @@ -0,0 +1,484 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: zones_v2 +// Service ID: 109 + +var _ resource.Resource = &ZonesV2Resource{} +var _ resource.ResourceWithModifyPlan = &ZonesV2Resource{} +var _ resource.ResourceWithImportState = &ZonesV2Resource{} + +type ZonesV2Resource struct { + client *core.UniversalClient +} + +type ZonesV2Model struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + Records types.String `tfsdk:"records"` + ResourceRealm types.String `tfsdk:"resource_realm"` + ZoneExpire types.Int64 `tfsdk:"zone_expire"` + ZoneKind types.String `tfsdk:"zone_kind"` + ZoneMastersForSlave types.String `tfsdk:"zone_masters_for_slave"` + ZoneName types.String `tfsdk:"zone_name"` + ZoneRefresh types.Int64 `tfsdk:"zone_refresh"` + ZoneRetry types.Int64 `tfsdk:"zone_retry"` + ZoneTTL types.Int64 `tfsdk:"zone_t_t_l"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewZonesV2Resource() resource.Resource { + return &ZonesV2Resource{} +} + +func (r *ZonesV2Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_zones_v2" +} + +func (r *ZonesV2Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "records": schema.StringAttribute{Required: true}, + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("powerdns"), MarkdownDescription: "Не изменяется после создания."}, + "zone_expire": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(3600000), MarkdownDescription: "Время в течение которого вторичный DNS будет пытаться завершить синхронизацию зоны с первичным Не изменяется после создания."}, + "zone_kind": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("NATIVE"), MarkdownDescription: "Тип зоны Не изменяется после создания."}, + "zone_masters_for_slave": schema.StringAttribute{Optional: true, MarkdownDescription: "Использовать если выбран тип SLAVE. IP адреса в формате через пробел для настройки Master-серверов Не изменяется после создания."}, + "zone_name": schema.StringAttribute{Required: true, MarkdownDescription: "Имя зоны Не изменяется после создания."}, + "zone_refresh": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(86400), MarkdownDescription: "Время ожидания ответа вторичного DNS перед запросом SOA-записи с первичных серверов Не изменяется после создания."}, + "zone_retry": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(7200), MarkdownDescription: "Время в течение которого сервер повторно попробует синхронизировать информацию с первичных DNS-серверов Не изменяется после создания."}, + "zone_t_t_l": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(86400), MarkdownDescription: "Время, в течение которого информация будет кешироваться другими DNS-серверами Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *ZonesV2Resource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *ZonesV2Model + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *ZonesV2Model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan ZonesV2Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneExpire.IsNull() && !plan.ZoneExpire.IsUnknown() && !state.ZoneExpire.IsNull() && !state.ZoneExpire.IsUnknown() { + if plan.ZoneExpire.ValueInt64() != state.ZoneExpire.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить zone_expire", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneKind.IsNull() && !plan.ZoneKind.IsUnknown() && !state.ZoneKind.IsNull() && !state.ZoneKind.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ZoneKind.ValueString(), state.ZoneKind.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить zone_kind", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneMastersForSlave.IsNull() && !plan.ZoneMastersForSlave.IsUnknown() && !state.ZoneMastersForSlave.IsNull() && !state.ZoneMastersForSlave.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ZoneMastersForSlave.ValueString(), state.ZoneMastersForSlave.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить zone_masters_for_slave", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneName.IsNull() && !plan.ZoneName.IsUnknown() && !state.ZoneName.IsNull() && !state.ZoneName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ZoneName.ValueString(), state.ZoneName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить zone_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneRefresh.IsNull() && !plan.ZoneRefresh.IsUnknown() && !state.ZoneRefresh.IsNull() && !state.ZoneRefresh.IsUnknown() { + if plan.ZoneRefresh.ValueInt64() != state.ZoneRefresh.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить zone_refresh", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneRetry.IsNull() && !plan.ZoneRetry.IsUnknown() && !state.ZoneRetry.IsNull() && !state.ZoneRetry.IsUnknown() { + if plan.ZoneRetry.ValueInt64() != state.ZoneRetry.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить zone_retry", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneTTL.IsNull() && !plan.ZoneTTL.IsUnknown() && !state.ZoneTTL.IsNull() && !state.ZoneTTL.IsUnknown() { + if plan.ZoneTTL.ValueInt64() != state.ZoneTTL.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить zone_t_t_l", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.Records.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "records is required.") + return + } + if config.Records.IsUnknown() { + return + } + if config.ZoneName.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "zone_name is required.") + return + } + if config.ZoneName.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 761: resources_core.FormatString(config.ZoneName), + 762: resources_core.FormatString(config.ZoneKind), + 763: resources_core.FormatString(config.ZoneMastersForSlave), + 764: resources_core.FormatInt64(config.ZoneRefresh), + 765: resources_core.FormatInt64(config.ZoneRetry), + 766: resources_core.FormatInt64(config.ZoneExpire), + 767: resources_core.FormatInt64(config.ZoneTTL), + 768: resources_core.FormatString(config.ResourceRealm), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 109, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *ZonesV2Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data ZonesV2Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.Records.IsNull() || data.Records.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "records is required.") + return + } + if data.ZoneName.IsNull() || data.ZoneName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "zone_name is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 109, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 761: resources_core.FormatString(data.ZoneName), + 762: resources_core.FormatString(data.ZoneKind), + 763: resources_core.FormatString(data.ZoneMastersForSlave), + 764: resources_core.FormatInt64(data.ZoneRefresh), + 765: resources_core.FormatInt64(data.ZoneRetry), + 766: resources_core.FormatInt64(data.ZoneExpire), + 767: resources_core.FormatInt64(data.ZoneTTL), + 768: resources_core.FormatString(data.ResourceRealm), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 109, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 109, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "records", Field: "Records", Type: "array-map-fixed"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "zoneExpire", Field: "ZoneExpire", Type: "int64"}, + {Code: "zoneKind", Field: "ZoneKind", Type: "string"}, + {Code: "zoneMastersForSlave", Field: "ZoneMastersForSlave", Type: "string"}, + {Code: "zoneName", Field: "ZoneName", Type: "string"}, + {Code: "zoneRefresh", Field: "ZoneRefresh", Type: "int64"}, + {Code: "zoneRetry", Field: "ZoneRetry", Type: "int64"}, + {Code: "zoneTTL", Field: "ZoneTTL", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *ZonesV2Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state ZonesV2Model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 109, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "records", Field: "Records", Type: "array-map-fixed"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "zoneExpire", Field: "ZoneExpire", Type: "int64"}, + {Code: "zoneKind", Field: "ZoneKind", Type: "string"}, + {Code: "zoneMastersForSlave", Field: "ZoneMastersForSlave", Type: "string"}, + {Code: "zoneName", Field: "ZoneName", Type: "string"}, + {Code: "zoneRefresh", Field: "ZoneRefresh", Type: "int64"}, + {Code: "zoneRetry", Field: "ZoneRetry", Type: "int64"}, + {Code: "zoneTTL", Field: "ZoneTTL", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *ZonesV2Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan ZonesV2Model + var state ZonesV2Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.Records.IsNull() != state.Records.IsNull() || plan.Records.IsUnknown() != state.Records.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.Records.IsNull() && !plan.Records.IsUnknown() && !state.Records.IsNull() && !state.Records.IsUnknown() { + if !strings.EqualFold(plan.Records.ValueString(), state.Records.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 769: resources_core.FormatString(plan.Records), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 109, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "records", Field: "Records", Type: "array-map-fixed"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "zoneExpire", Field: "ZoneExpire", Type: "int64"}, + {Code: "zoneKind", Field: "ZoneKind", Type: "string"}, + {Code: "zoneMastersForSlave", Field: "ZoneMastersForSlave", Type: "string"}, + {Code: "zoneName", Field: "ZoneName", Type: "string"}, + {Code: "zoneRefresh", Field: "ZoneRefresh", Type: "int64"}, + {Code: "zoneRetry", Field: "ZoneRetry", Type: "int64"}, + {Code: "zoneTTL", Field: "ZoneTTL", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *ZonesV2Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state ZonesV2Model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *ZonesV2Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *ZonesV2Resource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/110_dnszone_resource.go b/generated/test/go/110_dnszone_resource.go new file mode 100644 index 0000000..b6842a5 --- /dev/null +++ b/generated/test/go/110_dnszone_resource.go @@ -0,0 +1,495 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: dnszone +// Service ID: 110 + +var _ resource.Resource = &DnszoneResource{} +var _ resource.ResourceWithModifyPlan = &DnszoneResource{} +var _ resource.ResourceWithImportState = &DnszoneResource{} + +type DnszoneResource struct { + client *core.UniversalClient +} + +type DnszoneModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + ResourceRealm types.String `tfsdk:"resource_realm"` + ZoneEmail types.String `tfsdk:"zone_email"` + ZoneKind types.String `tfsdk:"zone_kind"` + ZoneMastersForSlave types.String `tfsdk:"zone_masters_for_slave"` + ZoneName types.String `tfsdk:"zone_name"` + ZoneTimeToExpire types.String `tfsdk:"zone_time_to_expire"` + ZoneTimeToLive types.String `tfsdk:"zone_time_to_live"` + ZoneTimeToRefresh types.String `tfsdk:"zone_time_to_refresh"` + ZoneTimeToRetry types.String `tfsdk:"zone_time_to_retry"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewDnszoneResource() resource.Resource { + return &DnszoneResource{} +} + +func (r *DnszoneResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_dnszone" +} + +func (r *DnszoneResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("powerdns"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + "zone_email": schema.StringAttribute{Required: true, MarkdownDescription: "Контактный e-mail Не изменяется после создания."}, + "zone_kind": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("NATIVE"), MarkdownDescription: "Тип зоны Не изменяется после создания."}, + "zone_masters_for_slave": schema.StringAttribute{Optional: true, MarkdownDescription: "Использовать если выбран тип SLAVE. IP адреса в формате через пробел для настройки Master-серверов Не изменяется после создания."}, + "zone_name": schema.StringAttribute{Required: true, MarkdownDescription: "Имя зоны Не изменяется после создания."}, + "zone_time_to_expire": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("3600000"), MarkdownDescription: "Время в течение которого вторичный DNS будет пытаться завершить синхронизацию зоны с первичным Не изменяется после создания."}, + "zone_time_to_live": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("86400"), MarkdownDescription: "Время, в течение которого информация будет кешироваться другими DNS-серверами Не изменяется после создания."}, + "zone_time_to_refresh": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("86400"), MarkdownDescription: "Время ожидания ответа вторичного DNS перед запросом SOA-записи с первичных серверов Не изменяется после создания."}, + "zone_time_to_retry": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("7200"), MarkdownDescription: "Время в течение которого сервер повторно попробует синхронизировать информацию с первичных DNS-серверов Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *DnszoneResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *DnszoneModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *DnszoneModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan DnszoneModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneEmail.IsNull() && !plan.ZoneEmail.IsUnknown() && !state.ZoneEmail.IsNull() && !state.ZoneEmail.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ZoneEmail.ValueString(), state.ZoneEmail.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить zone_email", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneKind.IsNull() && !plan.ZoneKind.IsUnknown() && !state.ZoneKind.IsNull() && !state.ZoneKind.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ZoneKind.ValueString(), state.ZoneKind.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить zone_kind", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneMastersForSlave.IsNull() && !plan.ZoneMastersForSlave.IsUnknown() && !state.ZoneMastersForSlave.IsNull() && !state.ZoneMastersForSlave.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ZoneMastersForSlave.ValueString(), state.ZoneMastersForSlave.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить zone_masters_for_slave", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneName.IsNull() && !plan.ZoneName.IsUnknown() && !state.ZoneName.IsNull() && !state.ZoneName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ZoneName.ValueString(), state.ZoneName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить zone_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneTimeToExpire.IsNull() && !plan.ZoneTimeToExpire.IsUnknown() && !state.ZoneTimeToExpire.IsNull() && !state.ZoneTimeToExpire.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ZoneTimeToExpire.ValueString(), state.ZoneTimeToExpire.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить zone_time_to_expire", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneTimeToLive.IsNull() && !plan.ZoneTimeToLive.IsUnknown() && !state.ZoneTimeToLive.IsNull() && !state.ZoneTimeToLive.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ZoneTimeToLive.ValueString(), state.ZoneTimeToLive.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить zone_time_to_live", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneTimeToRefresh.IsNull() && !plan.ZoneTimeToRefresh.IsUnknown() && !state.ZoneTimeToRefresh.IsNull() && !state.ZoneTimeToRefresh.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ZoneTimeToRefresh.ValueString(), state.ZoneTimeToRefresh.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить zone_time_to_refresh", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneTimeToRetry.IsNull() && !plan.ZoneTimeToRetry.IsUnknown() && !state.ZoneTimeToRetry.IsNull() && !state.ZoneTimeToRetry.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ZoneTimeToRetry.ValueString(), state.ZoneTimeToRetry.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить zone_time_to_retry", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.ZoneEmail.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "zone_email is required.") + return + } + if config.ZoneEmail.IsUnknown() { + return + } + if config.ZoneName.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "zone_name is required.") + return + } + if config.ZoneName.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 66: resources_core.FormatString(config.ZoneEmail), + 67: resources_core.FormatString(config.ZoneName), + 68: resources_core.FormatString(config.ZoneKind), + 69: resources_core.FormatString(config.ZoneMastersForSlave), + 70: resources_core.FormatString(config.ZoneTimeToRefresh), + 71: resources_core.FormatString(config.ZoneTimeToRetry), + 72: resources_core.FormatString(config.ZoneTimeToExpire), + 73: resources_core.FormatString(config.ZoneTimeToLive), + 284: resources_core.FormatString(config.ResourceRealm), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 110, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *DnszoneResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data DnszoneModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.ZoneEmail.IsNull() || data.ZoneEmail.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "zone_email is required.") + return + } + if data.ZoneName.IsNull() || data.ZoneName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "zone_name is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 110, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 66: resources_core.FormatString(data.ZoneEmail), + 67: resources_core.FormatString(data.ZoneName), + 68: resources_core.FormatString(data.ZoneKind), + 69: resources_core.FormatString(data.ZoneMastersForSlave), + 70: resources_core.FormatString(data.ZoneTimeToRefresh), + 71: resources_core.FormatString(data.ZoneTimeToRetry), + 72: resources_core.FormatString(data.ZoneTimeToExpire), + 73: resources_core.FormatString(data.ZoneTimeToLive), + 284: resources_core.FormatString(data.ResourceRealm), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 110, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 110, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "zoneEmail", Field: "ZoneEmail", Type: "string"}, + {Code: "zoneKind", Field: "ZoneKind", Type: "string"}, + {Code: "zoneMastersForSlave", Field: "ZoneMastersForSlave", Type: "string"}, + {Code: "zoneName", Field: "ZoneName", Type: "string"}, + {Code: "zoneTimeToExpire", Field: "ZoneTimeToExpire", Type: "string"}, + {Code: "zoneTimeToLive", Field: "ZoneTimeToLive", Type: "string"}, + {Code: "zoneTimeToRefresh", Field: "ZoneTimeToRefresh", Type: "string"}, + {Code: "zoneTimeToRetry", Field: "ZoneTimeToRetry", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *DnszoneResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state DnszoneModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 110, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "zoneEmail", Field: "ZoneEmail", Type: "string"}, + {Code: "zoneKind", Field: "ZoneKind", Type: "string"}, + {Code: "zoneMastersForSlave", Field: "ZoneMastersForSlave", Type: "string"}, + {Code: "zoneName", Field: "ZoneName", Type: "string"}, + {Code: "zoneTimeToExpire", Field: "ZoneTimeToExpire", Type: "string"}, + {Code: "zoneTimeToLive", Field: "ZoneTimeToLive", Type: "string"}, + {Code: "zoneTimeToRefresh", Field: "ZoneTimeToRefresh", Type: "string"}, + {Code: "zoneTimeToRetry", Field: "ZoneTimeToRetry", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *DnszoneResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan DnszoneModel + var state DnszoneModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 110, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "zoneEmail", Field: "ZoneEmail", Type: "string"}, + {Code: "zoneKind", Field: "ZoneKind", Type: "string"}, + {Code: "zoneMastersForSlave", Field: "ZoneMastersForSlave", Type: "string"}, + {Code: "zoneName", Field: "ZoneName", Type: "string"}, + {Code: "zoneTimeToExpire", Field: "ZoneTimeToExpire", Type: "string"}, + {Code: "zoneTimeToLive", Field: "ZoneTimeToLive", Type: "string"}, + {Code: "zoneTimeToRefresh", Field: "ZoneTimeToRefresh", Type: "string"}, + {Code: "zoneTimeToRetry", Field: "ZoneTimeToRetry", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *DnszoneResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state DnszoneModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *DnszoneResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *DnszoneResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/111_dnsrecord_resource.go b/generated/test/go/111_dnsrecord_resource.go new file mode 100644 index 0000000..4b988a5 --- /dev/null +++ b/generated/test/go/111_dnsrecord_resource.go @@ -0,0 +1,546 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: dnsrecord +// Service ID: 111 + +var _ resource.Resource = &DnsrecordResource{} +var _ resource.ResourceWithModifyPlan = &DnsrecordResource{} +var _ resource.ResourceWithImportState = &DnsrecordResource{} + +type DnsrecordResource struct { + client *core.UniversalClient +} + +type DnsrecordModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + RecordInput types.String `tfsdk:"record_input"` + RecordName types.String `tfsdk:"record_name"` + RecordTTL types.Int64 `tfsdk:"record_t_t_l"` + RecordType types.String `tfsdk:"record_type"` + ZoneUid types.String `tfsdk:"zone_uid"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewDnsrecordResource() resource.Resource { + return &DnsrecordResource{} +} + +func (r *DnsrecordResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_dnsrecord" +} + +func (r *DnsrecordResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "record_input": schema.StringAttribute{Required: true, MarkdownDescription: "Значение. Необходима валидация, в зависимости от типа записи. Должно быть заменено на UUID-внешнийIP"}, + "record_name": schema.StringAttribute{Required: true, MarkdownDescription: "Имя записи. Имя зоны в конце проставляется автоматически Не изменяется после создания."}, + "record_t_t_l": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(3600), MarkdownDescription: "Time-to-Leave записи. Раз в какое время запись будет обновлена Не изменяется после создания."}, + "record_type": schema.StringAttribute{Required: true, MarkdownDescription: "Тип DNS-записи Не изменяется после создания."}, + "zone_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UUID экземпляра услуги DNS-зоны Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *DnsrecordResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *DnsrecordModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *DnsrecordModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan DnsrecordModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan. + // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов. + // Resolve (displayName → UUID или uppercase → lowercase) нужен только для + // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка + // "Provider produced invalid plan: planned value does not match config value". + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.RecordName.IsNull() && !plan.RecordName.IsUnknown() && !state.RecordName.IsNull() && !state.RecordName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.RecordName.ValueString(), state.RecordName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить record_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.RecordTTL.IsNull() && !plan.RecordTTL.IsUnknown() && !state.RecordTTL.IsNull() && !state.RecordTTL.IsUnknown() { + if plan.RecordTTL.ValueInt64() != state.RecordTTL.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить record_t_t_l", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.RecordType.IsNull() && !plan.RecordType.IsUnknown() && !state.RecordType.IsNull() && !state.RecordType.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.RecordType.ValueString(), state.RecordType.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить record_type", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ZoneUid.IsNull() && !plan.ZoneUid.IsUnknown() && !state.ZoneUid.IsNull() && !state.ZoneUid.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ZoneUid.ValueString(), state.ZoneUid.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить zone_uid", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.RecordInput.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "record_input is required.") + return + } + if config.RecordInput.IsUnknown() { + return + } + if config.RecordName.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "record_name is required.") + return + } + if config.RecordName.IsUnknown() { + return + } + if config.RecordType.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "record_type is required.") + return + } + if config.RecordType.IsUnknown() { + return + } + if config.ZoneUid.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "zone_uid is required.") + return + } + if config.ZoneUid.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + if !config.ZoneUid.IsNull() && !config.ZoneUid.IsUnknown() { + resolvedZoneUid, err := r.client.ResolveRefSvcParamValue(ctx, 109, config.ZoneUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve zone_uid", err.Error()) + } else if resolvedZoneUid != "" && resolvedZoneUid != config.ZoneUid.ValueString() { + config.ZoneUid = types.StringValue(resolvedZoneUid) + } + } + params := map[int]string{ + 75: resources_core.FormatString(config.ZoneUid), + 76: resources_core.FormatString(config.RecordType), + 77: resources_core.FormatString(config.RecordName), + 78: resources_core.FormatString(config.RecordInput), + 450: resources_core.FormatInt64(config.RecordTTL), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 111, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *DnsrecordResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data DnsrecordModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.RecordInput.IsNull() || data.RecordInput.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "record_input is required.") + return + } + if data.RecordName.IsNull() || data.RecordName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "record_name is required.") + return + } + if data.RecordType.IsNull() || data.RecordType.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "record_type is required.") + return + } + if data.ZoneUid.IsNull() || data.ZoneUid.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "zone_uid is required.") + return + } + // ══════════════════════════════════════════════════════════════════════════ + // ПРАВИЛО TERRAFORM (официальная документация): + // «If an attribute value is configured, it is NEVER valid to change that + // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что + // написал пользователь). Менять plan запрещено на уровне фреймворка. + // + // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре. + // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный). + // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит: + // «Provider produced inconsistent result after apply». + // + // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот. + // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как + // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API). + // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный + // регистр в state через strings.EqualFold (сравниваем без учёта регистра). + // ══════════════════════════════════════════════════════════════════════════ + originalZoneUid := data.ZoneUid + if !data.ZoneUid.IsNull() && !data.ZoneUid.IsUnknown() { + resolvedZoneUid, err := r.client.ResolveRefSvcParamValue(ctx, 109, data.ZoneUid.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if resolvedZoneUid != "" && resolvedZoneUid != data.ZoneUid.ValueString() { + data.ZoneUid = types.StringValue(resolvedZoneUid) + } + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 111, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 75: resources_core.FormatString(data.ZoneUid), + 76: resources_core.FormatString(data.RecordType), + 77: resources_core.FormatString(data.RecordName), + 78: resources_core.FormatString(data.RecordInput), + 450: resources_core.FormatInt64(data.RecordTTL), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 111, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 111, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "recordInput", Field: "RecordInput", Type: "string"}, + {Code: "recordName", Field: "RecordName", Type: "string"}, + {Code: "recordTTL", Field: "RecordTTL", Type: "int64"}, + {Code: "recordType", Field: "RecordType", Type: "string"}, + {Code: "zoneUid", Field: "ZoneUid", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.ZoneUid.IsNull() && !state.ZoneUid.IsUnknown() { + resolvedZoneUid, err := r.client.ResolveRefSvcParamValue(ctx, 109, state.ZoneUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve zone_uid", err.Error()) + } else if resolvedZoneUid != "" && resolvedZoneUid != state.ZoneUid.ValueString() { + state.ZoneUid = types.StringValue(resolvedZoneUid) + } + } + // Restore user-provided casing в state (Create). + // EqualFold = «совпадают ли значения без учёта регистра?» + // Если да — значит API вернул «туже» строку, только в другом регистре. + // Заменяем state на original (то что было в plan/config пользователя). + // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint. + if !originalZoneUid.IsNull() && !originalZoneUid.IsUnknown() && !state.ZoneUid.IsNull() && !state.ZoneUid.IsUnknown() { + if strings.EqualFold(state.ZoneUid.ValueString(), originalZoneUid.ValueString()) { + state.ZoneUid = originalZoneUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *DnsrecordResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state DnsrecordModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 111, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "recordInput", Field: "RecordInput", Type: "string"}, + {Code: "recordName", Field: "RecordName", Type: "string"}, + {Code: "recordTTL", Field: "RecordTTL", Type: "int64"}, + {Code: "recordType", Field: "RecordType", Type: "string"}, + {Code: "zoneUid", Field: "ZoneUid", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !newState.ZoneUid.IsNull() && !newState.ZoneUid.IsUnknown() { + resolvedZoneUid, err := r.client.ResolveRefSvcParamValue(ctx, 109, newState.ZoneUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve zone_uid", err.Error()) + } else if resolvedZoneUid != "" && resolvedZoneUid != newState.ZoneUid.ValueString() { + newState.ZoneUid = types.StringValue(resolvedZoneUid) + } + } + // Restore user-provided casing в state (Read). + // При чтении у нас нет plan — но предыдущий state уже хранит значение + // в регистре пользователя (после первого Create оно было восстановлено). + // Берём prior state (переменная state) как эталон регистра. + // Если API вернул то же UUID только строчными буквами — восстанавливаем. + if !state.ZoneUid.IsNull() && !state.ZoneUid.IsUnknown() && !newState.ZoneUid.IsNull() && !newState.ZoneUid.IsUnknown() { + if strings.EqualFold(newState.ZoneUid.ValueString(), state.ZoneUid.ValueString()) { + newState.ZoneUid = state.ZoneUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *DnsrecordResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan DnsrecordModel + var state DnsrecordModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.RecordInput.IsNull() != state.RecordInput.IsNull() || plan.RecordInput.IsUnknown() != state.RecordInput.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.RecordInput.IsNull() && !plan.RecordInput.IsUnknown() && !state.RecordInput.IsNull() && !state.RecordInput.IsUnknown() { + if !strings.EqualFold(plan.RecordInput.ValueString(), state.RecordInput.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 256: resources_core.FormatString(plan.RecordInput), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 111, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "recordInput", Field: "RecordInput", Type: "string"}, + {Code: "recordName", Field: "RecordName", Type: "string"}, + {Code: "recordTTL", Field: "RecordTTL", Type: "int64"}, + {Code: "recordType", Field: "RecordType", Type: "string"}, + {Code: "zoneUid", Field: "ZoneUid", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.ZoneUid.IsNull() && !state.ZoneUid.IsUnknown() { + resolvedZoneUid, err := r.client.ResolveRefSvcParamValue(ctx, 109, state.ZoneUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve zone_uid", err.Error()) + } else if resolvedZoneUid != "" && resolvedZoneUid != state.ZoneUid.ValueString() { + state.ZoneUid = types.StringValue(resolvedZoneUid) + } + } + // Restore user-provided casing в state (Update). + // После API-вызова modify state содержит значения в lower-case от API. + // Plan == config == то что написал пользователь (регистр неизменён). + // EqualFold: если UUID совпадает без учёта регистра — берём из plan. + if !plan.ZoneUid.IsNull() && !plan.ZoneUid.IsUnknown() && !state.ZoneUid.IsNull() && !state.ZoneUid.IsUnknown() { + if strings.EqualFold(state.ZoneUid.ValueString(), plan.ZoneUid.ValueString()) { + state.ZoneUid = plan.ZoneUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *DnsrecordResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state DnsrecordModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *DnsrecordResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *DnsrecordResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/112_tenant_resource.go b/generated/test/go/112_tenant_resource.go new file mode 100644 index 0000000..dff6bb7 --- /dev/null +++ b/generated/test/go/112_tenant_resource.go @@ -0,0 +1,372 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: tenant +// Service ID: 112 + +var _ resource.Resource = &TenantResource{} +var _ resource.ResourceWithModifyPlan = &TenantResource{} +var _ resource.ResourceWithImportState = &TenantResource{} + +type TenantResource struct { + client *core.UniversalClient +} + +type TenantModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + GrafanaOrgName types.String `tfsdk:"grafana_org_name"` + ResourceRealm types.String `tfsdk:"resource_realm"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewTenantResource() resource.Resource { + return &TenantResource{} +} + +func (r *TenantResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_tenant" +} + +func (r *TenantResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "grafana_org_name": schema.StringAttribute{Required: true, MarkdownDescription: "WZ-Код клиента Не изменяется после создания."}, + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("grafana.ngcloud.ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *TenantResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *TenantModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *TenantModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan TenantModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.GrafanaOrgName.IsNull() && !plan.GrafanaOrgName.IsUnknown() && !state.GrafanaOrgName.IsNull() && !state.GrafanaOrgName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.GrafanaOrgName.ValueString(), state.GrafanaOrgName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить grafana_org_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.GrafanaOrgName.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "grafana_org_name is required.") + return + } + if config.GrafanaOrgName.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 239: resources_core.FormatString(config.GrafanaOrgName), + 240: resources_core.FormatString(config.ResourceRealm), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 112, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *TenantResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data TenantModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.GrafanaOrgName.IsNull() || data.GrafanaOrgName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "grafana_org_name is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 112, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 239: resources_core.FormatString(data.GrafanaOrgName), + 240: resources_core.FormatString(data.ResourceRealm), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 112, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 112, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "grafanaOrgName", Field: "GrafanaOrgName", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *TenantResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state TenantModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 112, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "grafanaOrgName", Field: "GrafanaOrgName", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *TenantResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan TenantModel + var state TenantModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 112, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "grafanaOrgName", Field: "GrafanaOrgName", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *TenantResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state TenantModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *TenantResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *TenantResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/113_vc_complex_resource.go b/generated/test/go/113_vc_complex_resource.go new file mode 100644 index 0000000..8f06254 --- /dev/null +++ b/generated/test/go/113_vc_complex_resource.go @@ -0,0 +1,563 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vc_complex +// Service ID: 113 + +var _ resource.Resource = &VcComplexResource{} +var _ resource.ResourceWithModifyPlan = &VcComplexResource{} +var _ resource.ResourceWithImportState = &VcComplexResource{} + +type VcComplexResource struct { + client *core.UniversalClient +} + +type VcComplexModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + CpuAllocated types.String `tfsdk:"cpu_allocated"` + CpuGuaranteed types.Int64 `tfsdk:"cpu_guaranteed"` + IpSpaceName types.String `tfsdk:"ip_space_name"` + MemAllocated types.Int64 `tfsdk:"mem_allocated"` + MemGuaranteed types.Int64 `tfsdk:"mem_guaranteed"` + NeedEnableAVI types.Bool `tfsdk:"need_enable_a_v_i"` + NeedExternalAddressSNAT types.Bool `tfsdk:"need_external_address_s_n_a_t"` + ResourceRealm types.String `tfsdk:"resource_realm"` + SegroupName types.String `tfsdk:"segroup_name"` + StorageConfig types.String `tfsdk:"storage_config"` + VIPCount types.Int64 `tfsdk:"v_i_p_count"` + VappName types.String `tfsdk:"vapp_name"` + VdcNetworkPool types.String `tfsdk:"vdc_network_pool"` + VdcProviderGateway types.String `tfsdk:"vdc_provider_gateway"` + VirtualServicesCount types.String `tfsdk:"virtual_services_count"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewVcComplexResource() resource.Resource { + return &VcComplexResource{} +} + +func (r *VcComplexResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vc_complex" +} + +func (r *VcComplexResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "cpu_allocated": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("1"), MarkdownDescription: "шт Не изменяется после создания."}, + "cpu_guaranteed": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(20), MarkdownDescription: "Не изменяется после создания."}, + "ip_space_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("internet-ipv4-v1"), MarkdownDescription: "Имя ip Space для внешнего IP. Целесообразнее брать самый дешёвый, так как это vip для исходящего трафика. Необходимо задавать если включен needExternalAddressSNAT Не изменяется после создания."}, + "mem_allocated": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "gb Не изменяется после создания."}, + "mem_guaranteed": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(20), MarkdownDescription: "Не изменяется после создания."}, + "need_enable_a_v_i": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включает AVI в Cloud Director Не изменяется после создания."}, + "need_external_address_s_n_a_t": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Выделяет внешний IP на Edge. Используется для SNAT всего трафика Не изменяется после создания."}, + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("sandbox.nubes.ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + "segroup_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("SEGROUP-SANDBOX-CL1-SHARED-01"), MarkdownDescription: "Наименование segroup, в котором будут выделяться пулы на AVI Не изменяется после создания."}, + "storage_config": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{\"name\": \"\", \"size\": 1024}]"), MarkdownDescription: "Описывается как массив объектов, где описывается имя политики и её размер Не изменяется после создания.", PlanModifiers: []planmodifier.String{resources_core.JsonNormalize()}}, + "v_i_p_count": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(0), MarkdownDescription: "ш Не изменяется после создания."}, + "vapp_name": schema.StringAttribute{Required: true, MarkdownDescription: "Имя vApp Не изменяется после создания."}, + "vdc_network_pool": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("nsxt-sandbox-geneve-np"), MarkdownDescription: "Не изменяется после создания."}, + "vdc_provider_gateway": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("sandbox-v1cl1-pvdc"), MarkdownDescription: "Взять из свойств ресурсной платформы.
Путь .vcd.providerVdc - взять KEY

providerGateway Cloud Director. Влияет ресурсы будут предоставлены. Является полем (List<String>) в свойствах рес. платформы ЛК, но сам параметр передаётся только как один String Не изменяется после создания."}, + "virtual_services_count": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("1"), MarkdownDescription: "Кол-во виртуальных сервисов, которые выделяются на AVI. Во избежании коллапса пока выделяется до 4 Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VcComplexResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *VcComplexModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *VcComplexModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan VcComplexModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.CpuAllocated.IsNull() && !plan.CpuAllocated.IsUnknown() && !state.CpuAllocated.IsNull() && !state.CpuAllocated.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.CpuAllocated.ValueString(), state.CpuAllocated.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить cpu_allocated", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.CpuGuaranteed.IsNull() && !plan.CpuGuaranteed.IsUnknown() && !state.CpuGuaranteed.IsNull() && !state.CpuGuaranteed.IsUnknown() { + if plan.CpuGuaranteed.ValueInt64() != state.CpuGuaranteed.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить cpu_guaranteed", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.IpSpaceName.IsNull() && !plan.IpSpaceName.IsUnknown() && !state.IpSpaceName.IsNull() && !state.IpSpaceName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.IpSpaceName.ValueString(), state.IpSpaceName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить ip_space_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.MemAllocated.IsNull() && !plan.MemAllocated.IsUnknown() && !state.MemAllocated.IsNull() && !state.MemAllocated.IsUnknown() { + if plan.MemAllocated.ValueInt64() != state.MemAllocated.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить mem_allocated", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.MemGuaranteed.IsNull() && !plan.MemGuaranteed.IsUnknown() && !state.MemGuaranteed.IsNull() && !state.MemGuaranteed.IsUnknown() { + if plan.MemGuaranteed.ValueInt64() != state.MemGuaranteed.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить mem_guaranteed", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.NeedEnableAVI.IsNull() && !plan.NeedEnableAVI.IsUnknown() && !state.NeedEnableAVI.IsNull() && !state.NeedEnableAVI.IsUnknown() { + if plan.NeedEnableAVI.ValueBool() != state.NeedEnableAVI.ValueBool() { + resp.Diagnostics.AddError("Нельзя изменить need_enable_a_v_i", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.NeedExternalAddressSNAT.IsNull() && !plan.NeedExternalAddressSNAT.IsUnknown() && !state.NeedExternalAddressSNAT.IsNull() && !state.NeedExternalAddressSNAT.IsUnknown() { + if plan.NeedExternalAddressSNAT.ValueBool() != state.NeedExternalAddressSNAT.ValueBool() { + resp.Diagnostics.AddError("Нельзя изменить need_external_address_s_n_a_t", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.SegroupName.IsNull() && !plan.SegroupName.IsUnknown() && !state.SegroupName.IsNull() && !state.SegroupName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.SegroupName.ValueString(), state.SegroupName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить segroup_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.StorageConfig.IsNull() && !plan.StorageConfig.IsUnknown() && !state.StorageConfig.IsNull() && !state.StorageConfig.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.StorageConfig.ValueString(), state.StorageConfig.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить storage_config", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.VIPCount.IsNull() && !plan.VIPCount.IsUnknown() && !state.VIPCount.IsNull() && !state.VIPCount.IsUnknown() { + if plan.VIPCount.ValueInt64() != state.VIPCount.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить v_i_p_count", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.VappName.IsNull() && !plan.VappName.IsUnknown() && !state.VappName.IsNull() && !state.VappName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VappName.ValueString(), state.VappName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить vapp_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.VdcNetworkPool.IsNull() && !plan.VdcNetworkPool.IsUnknown() && !state.VdcNetworkPool.IsNull() && !state.VdcNetworkPool.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VdcNetworkPool.ValueString(), state.VdcNetworkPool.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить vdc_network_pool", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.VdcProviderGateway.IsNull() && !plan.VdcProviderGateway.IsUnknown() && !state.VdcProviderGateway.IsNull() && !state.VdcProviderGateway.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VdcProviderGateway.ValueString(), state.VdcProviderGateway.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить vdc_provider_gateway", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.VirtualServicesCount.IsNull() && !plan.VirtualServicesCount.IsUnknown() && !state.VirtualServicesCount.IsNull() && !state.VirtualServicesCount.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VirtualServicesCount.ValueString(), state.VirtualServicesCount.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить virtual_services_count", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.VappName.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "vapp_name is required.") + return + } + if config.VappName.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 332: resources_core.FormatString(config.ResourceRealm), + 334: resources_core.FormatString(config.VappName), + 358: resources_core.FormatString(config.VdcProviderGateway), + 359: resources_core.FormatString(config.VirtualServicesCount), + 360: resources_core.FormatString(config.IpSpaceName), + 399: resources_core.FormatString(config.StorageConfig), + 400: resources_core.FormatString(config.VdcNetworkPool), + 401: resources_core.FormatInt64(config.CpuGuaranteed), + 402: resources_core.FormatInt64(config.MemGuaranteed), + 403: resources_core.FormatString(config.SegroupName), + 404: resources_core.FormatBool(config.NeedExternalAddressSNAT), + 405: resources_core.FormatBool(config.NeedEnableAVI), + 566: resources_core.FormatString(config.CpuAllocated), + 567: resources_core.FormatInt64(config.MemAllocated), + 568: resources_core.FormatInt64(config.VIPCount), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 113, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *VcComplexResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data VcComplexModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.VappName.IsNull() || data.VappName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "vapp_name is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 113, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 332: resources_core.FormatString(data.ResourceRealm), + 334: resources_core.FormatString(data.VappName), + 358: resources_core.FormatString(data.VdcProviderGateway), + 359: resources_core.FormatString(data.VirtualServicesCount), + 360: resources_core.FormatString(data.IpSpaceName), + 399: resources_core.FormatString(data.StorageConfig), + 400: resources_core.FormatString(data.VdcNetworkPool), + 401: resources_core.FormatInt64(data.CpuGuaranteed), + 402: resources_core.FormatInt64(data.MemGuaranteed), + 403: resources_core.FormatString(data.SegroupName), + 404: resources_core.FormatBool(data.NeedExternalAddressSNAT), + 405: resources_core.FormatBool(data.NeedEnableAVI), + 566: resources_core.FormatString(data.CpuAllocated), + 567: resources_core.FormatInt64(data.MemAllocated), + 568: resources_core.FormatInt64(data.VIPCount), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 113, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 113, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "cpuAllocated", Field: "CpuAllocated", Type: "string"}, + {Code: "cpuGuaranteed", Field: "CpuGuaranteed", Type: "int64"}, + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "memAllocated", Field: "MemAllocated", Type: "int64"}, + {Code: "memGuaranteed", Field: "MemGuaranteed", Type: "int64"}, + {Code: "needEnableAVI", Field: "NeedEnableAVI", Type: "bool"}, + {Code: "needExternalAddressSNAT", Field: "NeedExternalAddressSNAT", Type: "bool"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "segroupName", Field: "SegroupName", Type: "string"}, + {Code: "storageConfig", Field: "StorageConfig", Type: "string"}, + {Code: "vIPCount", Field: "VIPCount", Type: "int64"}, + {Code: "vappName", Field: "VappName", Type: "string"}, + {Code: "vdcNetworkPool", Field: "VdcNetworkPool", Type: "string"}, + {Code: "vdcProviderGateway", Field: "VdcProviderGateway", Type: "string"}, + {Code: "virtualServicesCount", Field: "VirtualServicesCount", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcComplexResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VcComplexModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 113, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "cpuAllocated", Field: "CpuAllocated", Type: "string"}, + {Code: "cpuGuaranteed", Field: "CpuGuaranteed", Type: "int64"}, + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "memAllocated", Field: "MemAllocated", Type: "int64"}, + {Code: "memGuaranteed", Field: "MemGuaranteed", Type: "int64"}, + {Code: "needEnableAVI", Field: "NeedEnableAVI", Type: "bool"}, + {Code: "needExternalAddressSNAT", Field: "NeedExternalAddressSNAT", Type: "bool"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "segroupName", Field: "SegroupName", Type: "string"}, + {Code: "storageConfig", Field: "StorageConfig", Type: "string"}, + {Code: "vIPCount", Field: "VIPCount", Type: "int64"}, + {Code: "vappName", Field: "VappName", Type: "string"}, + {Code: "vdcNetworkPool", Field: "VdcNetworkPool", Type: "string"}, + {Code: "vdcProviderGateway", Field: "VdcProviderGateway", Type: "string"}, + {Code: "virtualServicesCount", Field: "VirtualServicesCount", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *VcComplexResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VcComplexModel + var state VcComplexModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 113, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "cpuAllocated", Field: "CpuAllocated", Type: "string"}, + {Code: "cpuGuaranteed", Field: "CpuGuaranteed", Type: "int64"}, + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "memAllocated", Field: "MemAllocated", Type: "int64"}, + {Code: "memGuaranteed", Field: "MemGuaranteed", Type: "int64"}, + {Code: "needEnableAVI", Field: "NeedEnableAVI", Type: "bool"}, + {Code: "needExternalAddressSNAT", Field: "NeedExternalAddressSNAT", Type: "bool"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "segroupName", Field: "SegroupName", Type: "string"}, + {Code: "storageConfig", Field: "StorageConfig", Type: "string"}, + {Code: "vIPCount", Field: "VIPCount", Type: "int64"}, + {Code: "vappName", Field: "VappName", Type: "string"}, + {Code: "vdcNetworkPool", Field: "VdcNetworkPool", Type: "string"}, + {Code: "vdcProviderGateway", Field: "VdcProviderGateway", Type: "string"}, + {Code: "virtualServicesCount", Field: "VirtualServicesCount", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcComplexResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VcComplexModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *VcComplexResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *VcComplexResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/114_gitea_complex_resource.go b/generated/test/go/114_gitea_complex_resource.go new file mode 100644 index 0000000..6fb5f15 --- /dev/null +++ b/generated/test/go/114_gitea_complex_resource.go @@ -0,0 +1,322 @@ +package resources_gen + +import ( + "context" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: gitea_complex +// Service ID: 114 + +var _ resource.Resource = &GiteaComplexResource{} +var _ resource.ResourceWithModifyPlan = &GiteaComplexResource{} +var _ resource.ResourceWithImportState = &GiteaComplexResource{} + +type GiteaComplexResource struct { + client *core.UniversalClient +} + +type GiteaComplexModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewGiteaComplexResource() resource.Resource { + return &GiteaComplexResource{} +} + +func (r *GiteaComplexResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_gitea_complex" +} + +func (r *GiteaComplexResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *GiteaComplexResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *GiteaComplexModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *GiteaComplexModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan GiteaComplexModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{} + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 114, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *GiteaComplexResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data GiteaComplexModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 114, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{} + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 114, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 114, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{}) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *GiteaComplexResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state GiteaComplexModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 114, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{}) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *GiteaComplexResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan GiteaComplexModel + var state GiteaComplexModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 114, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{}) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *GiteaComplexResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state GiteaComplexModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *GiteaComplexResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *GiteaComplexResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/115_mariadb_database_resource.go b/generated/test/go/115_mariadb_database_resource.go new file mode 100644 index 0000000..ecce971 --- /dev/null +++ b/generated/test/go/115_mariadb_database_resource.go @@ -0,0 +1,343 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: mariadb +// Subresource: database + +var _ resource.Resource = &MariadbDatabaseResource{} + +// MariadbDatabaseResource manages a subresource via operations. +type MariadbDatabaseResource struct { + client *core.UniversalClient +} + +type MariadbDatabaseModel struct { + ID types.String `tfsdk:"id"` + MariadbID types.String `tfsdk:"mariadb_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + DbName types.String `tfsdk:"db_name"` +} + +func NewMariadbDatabaseResource() resource.Resource { + return &MariadbDatabaseResource{} +} + +func (r *MariadbDatabaseResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_mariadb_database" +} + +func (r *MariadbDatabaseResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "mariadb_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "db_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("db-bdwhrjlz"), MarkdownDescription: "Пример: `db01` Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *MariadbDatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan MariadbDatabaseModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.MariadbID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "dbName": resources_core.FormatString(plan.DbName), + } + identityCodes := []string{ + "dbName", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("database", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "dbName": resources_core.FormatString(plan.DbName), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_database", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_database", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *MariadbDatabaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state MariadbDatabaseModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *MariadbDatabaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan MariadbDatabaseModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state MariadbDatabaseModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.MariadbID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{ + "dbName": resources_core.FormatString(plan.DbName), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *MariadbDatabaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state MariadbDatabaseModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_database" == "" { + return + } + + instanceUID := strings.TrimSpace(state.MariadbID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "dbName": resources_core.FormatString(state.DbName), + } + identityCodes := []string{ + "dbName", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("database", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "dbName": resources_core.FormatString(state.DbName), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_database", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *MariadbDatabaseResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/115_mariadb_resource.go b/generated/test/go/115_mariadb_resource.go new file mode 100644 index 0000000..16798a3 --- /dev/null +++ b/generated/test/go/115_mariadb_resource.go @@ -0,0 +1,447 @@ +package resources_gen + +import ( + "context" + "fmt" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: mariadb +// Service ID: 115 + +var _ resource.Resource = &MariadbResource{} +var _ resource.ResourceWithModifyPlan = &MariadbResource{} +var _ resource.ResourceWithImportState = &MariadbResource{} + +type MariadbResource struct { + client *core.UniversalClient +} + +// MariadbAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration. +type MariadbAccessConfigurationModel struct { + MasterAccessList types.String `tfsdk:"master_access_list" json:"masterAccessList"` + MasterIpSpace types.String `tfsdk:"master_ip_space" json:"masterIpSpace"` +} + +// MariadbAutoscaleConfigurationModel — вложенная модель для map-fixed параметра autoscaleConfiguration. +type MariadbAutoscaleConfigurationModel struct { + Quota types.Int64 `tfsdk:"quota" json:"quota"` + Percent types.Int64 `tfsdk:"percent" json:"percent"` + Enabled types.Bool `tfsdk:"enabled" json:"enabled"` + Schedule types.Int64 `tfsdk:"schedule" json:"schedule"` +} + +// MariadbBackupConfigurationModel — вложенная модель для map-fixed параметра backupConfiguration. +type MariadbBackupConfigurationModel struct { + S3Uid types.String `tfsdk:"s3_uid" json:"s3Uid"` + Retain types.Int64 `tfsdk:"retain" json:"retain"` + Schedule types.String `tfsdk:"schedule" json:"schedule"` +} + +// MariadbClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type MariadbClusterConfigurationModel struct { + Cpu types.Int64 `tfsdk:"cpu" json:"cpu"` + Memory types.Int64 `tfsdk:"memory" json:"memory"` + Replicas types.Int64 `tfsdk:"replicas" json:"replicas"` + Disk types.Int64 `tfsdk:"disk" json:"disk"` +} + +// MariadbMariadbConfigurationModel — вложенная модель для map-fixed параметра mariadbConfiguration. +type MariadbMariadbConfigurationModel struct { + Version types.String `tfsdk:"version" json:"version"` +} + +// MariadbStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type MariadbStartupConfigurationModel struct { + ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"` +} + +type MariadbModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessConfiguration *MariadbAccessConfigurationModel `tfsdk:"access_configuration"` + AutoscaleConfiguration *MariadbAutoscaleConfigurationModel `tfsdk:"autoscale_configuration"` + BackupConfiguration *MariadbBackupConfigurationModel `tfsdk:"backup_configuration"` + ClusterConfiguration *MariadbClusterConfigurationModel `tfsdk:"cluster_configuration"` + MariadbConfiguration *MariadbMariadbConfigurationModel `tfsdk:"mariadb_configuration"` + StartupConfiguration *MariadbStartupConfigurationModel `tfsdk:"startup_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewMariadbResource() resource.Resource { + return &MariadbResource{} +} + +func (r *MariadbResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_mariadb" +} + +func (r *MariadbResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"}, + "master_ip_space": schema.StringAttribute{Required: true, MarkdownDescription: "Из какого Ip-Space резервировать внешний IP
Список предоставляется из Организации, в котором развёрнута ресурсная платформа"}, + }}, + "autoscale_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "quota": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100), MarkdownDescription: "До какого размера возможно увеличивать диск, когда включен Autoscale"}, + "percent": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Требует включения autoScale. Позволяет указать в процентах расширение от текущего макс объема
Минимальный размер на который расширяется - **1Gb**"}, + "enabled": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включает автоскейлинг PV. При включении параметра необходимо настроить autoScalePercentage и autoScaleTechWindow. Алерт при срабатывании вызывает modify у текущей услуги"}, + "schedule": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(0), MarkdownDescription: "Требует включения autoScale. Окно обновления (пока не реализовано). Указывается час в виде Integer (0 / 5 / 12 / 23)"}, + }}, + "backup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "s3_uid": schema.StringAttribute{Required: true, MarkdownDescription: "Экземпляр S3 обязателен"}, + "retain": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(720)}, + "schedule": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("0 0 * * *")}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"}, + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Указывается в Megabytes"}, + "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Пока `Galera` выключена
Как будет запрос на включение и тестирование, реплики можно будет увеличивать"}, + "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Указывается в GIGAbytes"}, + }}, + "mariadb_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("9.4.0")}, + }}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"}, + }}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *MariadbResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *MariadbModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *MariadbModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan MariadbModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 877: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}), + 878: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64())}), + 879: resources_core.BuildJSON(map[string]string{"masterAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterAccessList.ValueString()), "masterIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterIpSpace.ValueString())}), + 880: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.MariadbConfiguration.Version.ValueString())}), + 881: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", config.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", config.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", config.BackupConfiguration.Schedule.ValueString())}), + 882: resources_core.BuildJSON(map[string]string{"quota": fmt.Sprintf("%d", config.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", config.AutoscaleConfiguration.Percent.ValueInt64()), "enabled": fmt.Sprintf("%v", config.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", config.AutoscaleConfiguration.Schedule.ValueInt64())}), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 115, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *MariadbResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data MariadbModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 115, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 877: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}), + 878: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64())}), + 879: resources_core.BuildJSON(map[string]string{"masterAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterAccessList.ValueString()), "masterIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterIpSpace.ValueString())}), + 880: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.MariadbConfiguration.Version.ValueString())}), + 881: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", data.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", data.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", data.BackupConfiguration.Schedule.ValueString())}), + 882: resources_core.BuildJSON(map[string]string{"quota": fmt.Sprintf("%d", data.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", data.AutoscaleConfiguration.Percent.ValueInt64()), "enabled": fmt.Sprintf("%v", data.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", data.AutoscaleConfiguration.Schedule.ValueInt64())}), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 115, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 115, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"}, + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "mariadbConfiguration", Field: "MariadbConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *MariadbResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state MariadbModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 115, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"}, + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "mariadbConfiguration", Field: "MariadbConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *MariadbResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan MariadbModel + var state MariadbModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 883: resources_core.BuildJSON(map[string]string{"replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64())}), + 884: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterAccessList.ValueString())}), + 885: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", plan.MariadbConfiguration.Version.ValueString())}), + 886: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", plan.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", plan.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", plan.BackupConfiguration.Schedule.ValueString())}), + 887: resources_core.BuildJSON(map[string]string{"percent": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Percent.ValueInt64()), "enabled": fmt.Sprintf("%v", plan.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Quota.ValueInt64())}), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 115, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"}, + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "mariadbConfiguration", Field: "MariadbConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *MariadbResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state MariadbModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *MariadbResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *MariadbResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/115_mariadb_user_resource.go b/generated/test/go/115_mariadb_user_resource.go new file mode 100644 index 0000000..5f74ba8 --- /dev/null +++ b/generated/test/go/115_mariadb_user_resource.go @@ -0,0 +1,367 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: mariadb +// Subresource: user + +var _ resource.Resource = &MariadbUserResource{} + +// MariadbUserResource manages a subresource via operations. +type MariadbUserResource struct { + client *core.UniversalClient +} + +type MariadbUserModel struct { + ID types.String `tfsdk:"id"` + MariadbID types.String `tfsdk:"mariadb_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessHosts types.String `tfsdk:"access_hosts"` + DbName types.String `tfsdk:"db_name"` + Password types.String `tfsdk:"password"` + Username types.String `tfsdk:"username"` +} + +func NewMariadbUserResource() resource.Resource { + return &MariadbUserResource{} +} + +func (r *MariadbUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_mariadb_user" +} + +func (r *MariadbUserResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "mariadb_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_hosts": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("%"), MarkdownDescription: "Пример: `%` Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "db_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Необходимо указать массив существующих баз Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "password": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("dz8vtf8o"), MarkdownDescription: "Секрет будет записан в хранилище секретов и не будет отображаться публично Не изменяется после создания. Изменение требует пересоздания.", Sensitive: true, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "username": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("user2ktdtvd8"), MarkdownDescription: "Пример: `myusername` Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *MariadbUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan MariadbUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.MariadbID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "accessHosts": resources_core.FormatString(plan.AccessHosts), + "dbName": resources_core.FormatString(plan.DbName), + "password": resources_core.FormatString(plan.Password), + "username": resources_core.FormatString(plan.Username), + } + identityCodes := []string{ + "accessHosts", + "dbName", + "password", + "username", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "username": resources_core.FormatString(plan.Username), + "password": resources_core.FormatString(plan.Password), + "dbName": resources_core.FormatString(plan.DbName), + "accessHosts": resources_core.FormatString(plan.AccessHosts), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *MariadbUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state MariadbUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *MariadbUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan MariadbUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state MariadbUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.MariadbID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{ + "accessHosts": resources_core.FormatString(plan.AccessHosts), + "dbName": resources_core.FormatString(plan.DbName), + "password": resources_core.FormatString(plan.Password), + "username": resources_core.FormatString(plan.Username), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *MariadbUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state MariadbUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_user" == "" { + return + } + + instanceUID := strings.TrimSpace(state.MariadbID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "accessHosts": resources_core.FormatString(state.AccessHosts), + "dbName": resources_core.FormatString(state.DbName), + "password": resources_core.FormatString(state.Password), + "username": resources_core.FormatString(state.Username), + } + identityCodes := []string{ + "accessHosts", + "dbName", + "password", + "username", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "username": resources_core.FormatString(state.Username), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_user", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *MariadbUserResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/116_kafka_resource.go b/generated/test/go/116_kafka_resource.go new file mode 100644 index 0000000..746405d --- /dev/null +++ b/generated/test/go/116_kafka_resource.go @@ -0,0 +1,389 @@ +package resources_gen + +import ( + "context" + "fmt" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: kafka +// Service ID: 116 + +var _ resource.Resource = &KafkaResource{} +var _ resource.ResourceWithModifyPlan = &KafkaResource{} +var _ resource.ResourceWithImportState = &KafkaResource{} + +type KafkaResource struct { + client *core.UniversalClient +} + +// KafkaAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration. +type KafkaAccessConfigurationModel struct { + MasterAccessList types.String `tfsdk:"master_access_list" json:"masterAccessList"` + AllocationExternal types.Bool `tfsdk:"allocation_external" json:"allocationExternal"` +} + +// KafkaClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type KafkaClusterConfigurationModel struct { + Cpu types.Int64 `tfsdk:"cpu" json:"cpu"` + Memory types.Int64 `tfsdk:"memory" json:"memory"` + Replicas types.Int64 `tfsdk:"replicas" json:"replicas"` + Disk types.Int64 `tfsdk:"disk" json:"disk"` +} + +// KafkaStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type KafkaStartupConfigurationModel struct { + ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"` +} + +type KafkaModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessConfiguration *KafkaAccessConfigurationModel `tfsdk:"access_configuration"` + ClusterConfiguration *KafkaClusterConfigurationModel `tfsdk:"cluster_configuration"` + StartupConfiguration *KafkaStartupConfigurationModel `tfsdk:"startup_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewKafkaResource() resource.Resource { + return &KafkaResource{} +} + +func (r *KafkaResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_kafka" +} + +func (r *KafkaResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"}, + "allocation_external": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2000), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"}, + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2048)}, + "replicas": schema.Int64Attribute{Required: true}, + "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)}, + }}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"}, + }}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *KafkaResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *KafkaModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *KafkaModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan KafkaModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 1084: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}), + 1085: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64())}), + 1086: resources_core.BuildJSON(map[string]string{"masterAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterAccessList.ValueString()), "allocationExternal": fmt.Sprintf("%v", config.AccessConfiguration.AllocationExternal.ValueBool())}), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 116, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *KafkaResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data KafkaModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 116, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 1084: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}), + 1085: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64())}), + 1086: resources_core.BuildJSON(map[string]string{"masterAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterAccessList.ValueString()), "allocationExternal": fmt.Sprintf("%v", data.AccessConfiguration.AllocationExternal.ValueBool())}), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 116, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 116, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *KafkaResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state KafkaModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 116, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *KafkaResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan KafkaModel + var state KafkaModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 1087: resources_core.BuildJSON(map[string]string{"replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64())}), + 1088: resources_core.BuildJSON(map[string]string{"allocationExternal": fmt.Sprintf("%v", plan.AccessConfiguration.AllocationExternal.ValueBool()), "masterAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterAccessList.ValueString())}), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 116, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *KafkaResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state KafkaModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *KafkaResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *KafkaResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/116_kafka_topic_resource.go b/generated/test/go/116_kafka_topic_resource.go new file mode 100644 index 0000000..3f289b3 --- /dev/null +++ b/generated/test/go/116_kafka_topic_resource.go @@ -0,0 +1,364 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: kafka +// Subresource: topic + +var _ resource.Resource = &KafkaTopicResource{} + +// KafkaTopicResource manages a subresource via operations. +type KafkaTopicResource struct { + client *core.UniversalClient +} + +type KafkaTopicModel struct { + ID types.String `tfsdk:"id"` + KafkaID types.String `tfsdk:"kafka_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + NameTopic types.String `tfsdk:"name_topic"` + Partitions types.Int64 `tfsdk:"partitions"` + Replicas types.Int64 `tfsdk:"replicas"` +} + +func NewKafkaTopicResource() resource.Resource { + return &KafkaTopicResource{} +} + +func (r *KafkaTopicResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_kafka_topic" +} + +func (r *KafkaTopicResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "kafka_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "name_topic": schema.StringAttribute{Required: true, MarkdownDescription: "Пример: `orders-events` Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "partitions": schema.Int64Attribute{Required: true, MarkdownDescription: "Число партиций можно увеличить, но уменьшить их нельзя."}, + "replicas": schema.Int64Attribute{Required: true, MarkdownDescription: "Количество реплик не может привышать количество реплик брокера"}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *KafkaTopicResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan KafkaTopicModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if plan.NameTopic.IsNull() || plan.NameTopic.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "name_topic is required.") + return + } + if plan.Partitions.IsNull() || plan.Partitions.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "partitions is required.") + return + } + if plan.Replicas.IsNull() || plan.Replicas.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "replicas is required.") + return + } + + instanceUID := strings.TrimSpace(plan.KafkaID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "nameTopic": resources_core.FormatString(plan.NameTopic), + } + identityCodes := []string{ + "nameTopic", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("topic", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "topic", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "nameTopic": resources_core.FormatString(plan.NameTopic), + "partitions": resources_core.FormatInt64(plan.Partitions), + "replicas": resources_core.FormatInt64(plan.Replicas), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_topic", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_topic", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "topic", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "topic", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "topic", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *KafkaTopicResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state KafkaTopicModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *KafkaTopicResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan KafkaTopicModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "modify_topic" == "" { + var state KafkaTopicModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.KafkaID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{ + "nameTopic": resources_core.FormatString(plan.NameTopic), + "partitions": resources_core.FormatInt64(plan.Partitions), + "replicas": resources_core.FormatInt64(plan.Replicas), + }) + + idParams := map[string]string{ + "nameTopic": resources_core.FormatString(plan.NameTopic), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "modify_topic", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "topic", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *KafkaTopicResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state KafkaTopicModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_topic" == "" { + return + } + + instanceUID := strings.TrimSpace(state.KafkaID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "nameTopic": resources_core.FormatString(state.NameTopic), + } + identityCodes := []string{ + "nameTopic", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("topic", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "nameTopic": resources_core.FormatString(state.NameTopic), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_topic", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *KafkaTopicResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/116_kafka_user_resource.go b/generated/test/go/116_kafka_user_resource.go new file mode 100644 index 0000000..52ea73c --- /dev/null +++ b/generated/test/go/116_kafka_user_resource.go @@ -0,0 +1,373 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: kafka +// Subresource: user + +var _ resource.Resource = &KafkaUserResource{} + +// KafkaUserResource manages a subresource via operations. +type KafkaUserResource struct { + client *core.UniversalClient +} + +type KafkaUserModel struct { + ID types.String `tfsdk:"id"` + KafkaID types.String `tfsdk:"kafka_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessHosts types.String `tfsdk:"access_hosts"` + Group types.String `tfsdk:"group"` + NameTopic types.String `tfsdk:"name_topic"` + Operations types.String `tfsdk:"operations"` + Username types.String `tfsdk:"username"` +} + +func NewKafkaUserResource() resource.Resource { + return &KafkaUserResource{} +} + +func (r *KafkaUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_kafka_user" +} + +func (r *KafkaUserResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "kafka_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_hosts": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("*"), MarkdownDescription: "Пример: `192.168.1.0/24` или `*`"}, + "group": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("*"), MarkdownDescription: "Пример: `order-consumers` или `*`"}, + "name_topic": schema.StringAttribute{Required: true, MarkdownDescription: "Пример: `orders-events`. Использовать маску нельзя(`orders*`), только полное название или `*`"}, + "operations": schema.StringAttribute{Required: true, MarkdownDescription: "Пример: `Create, Describe, Read, Write`"}, + "username": schema.StringAttribute{Required: true, MarkdownDescription: "Пример: `username-admin` Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *KafkaUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan KafkaUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if plan.NameTopic.IsNull() || plan.NameTopic.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "name_topic is required.") + return + } + if plan.Operations.IsNull() || plan.Operations.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "operations is required.") + return + } + if plan.Username.IsNull() || plan.Username.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "username is required.") + return + } + + instanceUID := strings.TrimSpace(plan.KafkaID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "username": resources_core.FormatString(plan.Username), + } + identityCodes := []string{ + "username", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "username": resources_core.FormatString(plan.Username), + "nameTopic": resources_core.FormatString(plan.NameTopic), + "operations": resources_core.FormatString(plan.Operations), + "group": resources_core.FormatString(plan.Group), + "accessHosts": resources_core.FormatString(plan.AccessHosts), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *KafkaUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state KafkaUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *KafkaUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan KafkaUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "modify_user" == "" { + var state KafkaUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.KafkaID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{ + "nameTopic": resources_core.FormatString(plan.NameTopic), + "operations": resources_core.FormatString(plan.Operations), + "group": resources_core.FormatString(plan.Group), + "accessHosts": resources_core.FormatString(plan.AccessHosts), + "username": resources_core.FormatString(plan.Username), + }) + + idParams := map[string]string{ + "username": resources_core.FormatString(plan.Username), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "modify_user", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *KafkaUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state KafkaUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_user" == "" { + return + } + + instanceUID := strings.TrimSpace(state.KafkaID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "username": resources_core.FormatString(state.Username), + } + identityCodes := []string{ + "username", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "username": resources_core.FormatString(state.Username), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_user", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *KafkaUserResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/117_nifi_resource.go b/generated/test/go/117_nifi_resource.go new file mode 100644 index 0000000..c2886ce --- /dev/null +++ b/generated/test/go/117_nifi_resource.go @@ -0,0 +1,533 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: nifi +// Service ID: 117 + +var _ resource.Resource = &NifiResource{} +var _ resource.ResourceWithModifyPlan = &NifiResource{} +var _ resource.ResourceWithImportState = &NifiResource{} + +type NifiResource struct { + client *core.UniversalClient +} + +type NifiModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + KafkaUid types.String `tfsdk:"kafka_uid"` + NameTopic types.String `tfsdk:"name_topic"` + Partitions types.Int64 `tfsdk:"partitions"` + Replicas types.Int64 `tfsdk:"replicas"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewNifiResource() resource.Resource { + return &NifiResource{} +} + +func (r *NifiResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_nifi" +} + +func (r *NifiResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "kafka_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "Выбрать кластер из списка Не изменяется после создания."}, + "name_topic": schema.StringAttribute{Required: true, MarkdownDescription: "Пример: `orders-events` Не изменяется после создания."}, + "partitions": schema.Int64Attribute{Required: true, MarkdownDescription: "Число партиций на кластер ≤ 100 × (число брокеров) × (фактор репликации). Пример `50`"}, + "replicas": schema.Int64Attribute{Required: true, MarkdownDescription: "Количество партиций ≤ (число брокеров). Пример `2`"}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *NifiResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *NifiModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *NifiModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan NifiModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan. + // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов. + // Resolve (displayName → UUID или uppercase → lowercase) нужен только для + // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка + // "Provider produced invalid plan: planned value does not match config value". + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.KafkaUid.IsNull() && !plan.KafkaUid.IsUnknown() && !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.KafkaUid.ValueString(), state.KafkaUid.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить kafka_uid", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.NameTopic.IsNull() && !plan.NameTopic.IsUnknown() && !state.NameTopic.IsNull() && !state.NameTopic.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.NameTopic.ValueString(), state.NameTopic.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить name_topic", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.KafkaUid.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "kafka_uid is required.") + return + } + if config.KafkaUid.IsUnknown() { + return + } + if config.NameTopic.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "name_topic is required.") + return + } + if config.NameTopic.IsUnknown() { + return + } + if config.Partitions.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "partitions is required.") + return + } + if config.Partitions.IsUnknown() { + return + } + if config.Replicas.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "replicas is required.") + return + } + if config.Replicas.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + if !config.KafkaUid.IsNull() && !config.KafkaUid.IsUnknown() { + resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, config.KafkaUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve kafka_uid", err.Error()) + } else if resolvedKafkaUid != "" && resolvedKafkaUid != config.KafkaUid.ValueString() { + config.KafkaUid = types.StringValue(resolvedKafkaUid) + } + } + params := map[int]string{ + 471: resources_core.FormatString(config.KafkaUid), + 472: resources_core.FormatInt64(config.Partitions), + 473: resources_core.FormatInt64(config.Replicas), + 474: resources_core.FormatString(config.NameTopic), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 117, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *NifiResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data NifiModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.KafkaUid.IsNull() || data.KafkaUid.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "kafka_uid is required.") + return + } + if data.NameTopic.IsNull() || data.NameTopic.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "name_topic is required.") + return + } + if data.Partitions.IsNull() || data.Partitions.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "partitions is required.") + return + } + if data.Replicas.IsNull() || data.Replicas.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "replicas is required.") + return + } + // ══════════════════════════════════════════════════════════════════════════ + // ПРАВИЛО TERRAFORM (официальная документация): + // «If an attribute value is configured, it is NEVER valid to change that + // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что + // написал пользователь). Менять plan запрещено на уровне фреймворка. + // + // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре. + // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный). + // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит: + // «Provider produced inconsistent result after apply». + // + // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот. + // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как + // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API). + // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный + // регистр в state через strings.EqualFold (сравниваем без учёта регистра). + // ══════════════════════════════════════════════════════════════════════════ + originalKafkaUid := data.KafkaUid + if !data.KafkaUid.IsNull() && !data.KafkaUid.IsUnknown() { + resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, data.KafkaUid.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if resolvedKafkaUid != "" && resolvedKafkaUid != data.KafkaUid.ValueString() { + data.KafkaUid = types.StringValue(resolvedKafkaUid) + } + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 117, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 471: resources_core.FormatString(data.KafkaUid), + 472: resources_core.FormatInt64(data.Partitions), + 473: resources_core.FormatInt64(data.Replicas), + 474: resources_core.FormatString(data.NameTopic), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 117, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 117, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "kafkaUid", Field: "KafkaUid", Type: "string"}, + {Code: "nameTopic", Field: "NameTopic", Type: "string"}, + {Code: "partitions", Field: "Partitions", Type: "int64"}, + {Code: "replicas", Field: "Replicas", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() { + resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, state.KafkaUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve kafka_uid", err.Error()) + } else if resolvedKafkaUid != "" && resolvedKafkaUid != state.KafkaUid.ValueString() { + state.KafkaUid = types.StringValue(resolvedKafkaUid) + } + } + // Restore user-provided casing в state (Create). + // EqualFold = «совпадают ли значения без учёта регистра?» + // Если да — значит API вернул «туже» строку, только в другом регистре. + // Заменяем state на original (то что было в plan/config пользователя). + // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint. + if !originalKafkaUid.IsNull() && !originalKafkaUid.IsUnknown() && !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() { + if strings.EqualFold(state.KafkaUid.ValueString(), originalKafkaUid.ValueString()) { + state.KafkaUid = originalKafkaUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *NifiResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state NifiModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 117, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "kafkaUid", Field: "KafkaUid", Type: "string"}, + {Code: "nameTopic", Field: "NameTopic", Type: "string"}, + {Code: "partitions", Field: "Partitions", Type: "int64"}, + {Code: "replicas", Field: "Replicas", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !newState.KafkaUid.IsNull() && !newState.KafkaUid.IsUnknown() { + resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, newState.KafkaUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve kafka_uid", err.Error()) + } else if resolvedKafkaUid != "" && resolvedKafkaUid != newState.KafkaUid.ValueString() { + newState.KafkaUid = types.StringValue(resolvedKafkaUid) + } + } + // Restore user-provided casing в state (Read). + // При чтении у нас нет plan — но предыдущий state уже хранит значение + // в регистре пользователя (после первого Create оно было восстановлено). + // Берём prior state (переменная state) как эталон регистра. + // Если API вернул то же UUID только строчными буквами — восстанавливаем. + if !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() && !newState.KafkaUid.IsNull() && !newState.KafkaUid.IsUnknown() { + if strings.EqualFold(newState.KafkaUid.ValueString(), state.KafkaUid.ValueString()) { + newState.KafkaUid = state.KafkaUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *NifiResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan NifiModel + var state NifiModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.Partitions.IsNull() != state.Partitions.IsNull() || plan.Partitions.IsUnknown() != state.Partitions.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.Partitions.IsNull() && !plan.Partitions.IsUnknown() && !state.Partitions.IsNull() && !state.Partitions.IsUnknown() { + if plan.Partitions.ValueInt64() != state.Partitions.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.Replicas.IsNull() != state.Replicas.IsNull() || plan.Replicas.IsUnknown() != state.Replicas.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.Replicas.IsNull() && !plan.Replicas.IsUnknown() && !state.Replicas.IsNull() && !state.Replicas.IsUnknown() { + if plan.Replicas.ValueInt64() != state.Replicas.ValueInt64() { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 477: resources_core.FormatInt64(plan.Partitions), + 478: resources_core.FormatInt64(plan.Replicas), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 117, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "kafkaUid", Field: "KafkaUid", Type: "string"}, + {Code: "nameTopic", Field: "NameTopic", Type: "string"}, + {Code: "partitions", Field: "Partitions", Type: "int64"}, + {Code: "replicas", Field: "Replicas", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() { + resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, state.KafkaUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve kafka_uid", err.Error()) + } else if resolvedKafkaUid != "" && resolvedKafkaUid != state.KafkaUid.ValueString() { + state.KafkaUid = types.StringValue(resolvedKafkaUid) + } + } + // Restore user-provided casing в state (Update). + // После API-вызова modify state содержит значения в lower-case от API. + // Plan == config == то что написал пользователь (регистр неизменён). + // EqualFold: если UUID совпадает без учёта регистра — берём из plan. + if !plan.KafkaUid.IsNull() && !plan.KafkaUid.IsUnknown() && !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() { + if strings.EqualFold(state.KafkaUid.ValueString(), plan.KafkaUid.ValueString()) { + state.KafkaUid = plan.KafkaUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *NifiResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state NifiModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *NifiResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *NifiResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/119_akhq_resource.go b/generated/test/go/119_akhq_resource.go new file mode 100644 index 0000000..8ce242b --- /dev/null +++ b/generated/test/go/119_akhq_resource.go @@ -0,0 +1,554 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: akhq +// Service ID: 119 + +var _ resource.Resource = &AkhqResource{} +var _ resource.ResourceWithModifyPlan = &AkhqResource{} +var _ resource.ResourceWithImportState = &AkhqResource{} + +type AkhqResource struct { + client *core.UniversalClient +} + +type AkhqModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + Domain types.String `tfsdk:"domain"` + KafkaUid types.String `tfsdk:"kafka_uid"` + ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"` + ResourceInstances types.Int64 `tfsdk:"resource_instances"` + ResourceMemory types.Int64 `tfsdk:"resource_memory"` + ResourceRealm types.String `tfsdk:"resource_realm"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewAkhqResource() resource.Resource { + return &AkhqResource{} +} + +func (r *AkhqResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_akhq" +} + +func (r *AkhqResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "domain": schema.StringAttribute{Required: true, MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."}, + "kafka_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "Выбрать кластер из списка"}, + "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Пример: `1000` Не изменяется после создания."}, + "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Не изменяется после создания."}, + "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Пример: `1024` Не изменяется после создания."}, + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Выбрать платформу из списка Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *AkhqResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *AkhqModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *AkhqModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan AkhqModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan. + // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов. + // Resolve (displayName → UUID или uppercase → lowercase) нужен только для + // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка + // "Provider produced invalid plan: planned value does not match config value". + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() { + if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_c_p_u", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() { + if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_instances", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() { + if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_memory", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.Domain.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "domain is required.") + return + } + if config.Domain.IsUnknown() { + return + } + if config.KafkaUid.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "kafka_uid is required.") + return + } + if config.KafkaUid.IsUnknown() { + return + } + if config.ResourceRealm.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + return + } + if config.ResourceRealm.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + if !config.KafkaUid.IsNull() && !config.KafkaUid.IsUnknown() { + resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, config.KafkaUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve kafka_uid", err.Error()) + } else if resolvedKafkaUid != "" && resolvedKafkaUid != config.KafkaUid.ValueString() { + config.KafkaUid = types.StringValue(resolvedKafkaUid) + } + } + params := map[int]string{ + 550: resources_core.FormatString(config.ResourceRealm), + 551: resources_core.FormatInt64(config.ResourceMemory), + 552: resources_core.FormatInt64(config.ResourceCPU), + 553: resources_core.FormatString(config.Domain), + 554: resources_core.FormatInt64(config.ResourceInstances), + 555: resources_core.FormatString(config.KafkaUid), + } + desiredDomain := "" + if !config.Domain.IsNull() && !config.Domain.IsUnknown() { + desiredDomain = config.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 119, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *AkhqResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data AkhqModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.Domain.IsNull() || data.Domain.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "domain is required.") + return + } + if data.KafkaUid.IsNull() || data.KafkaUid.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "kafka_uid is required.") + return + } + if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + return + } + // ══════════════════════════════════════════════════════════════════════════ + // ПРАВИЛО TERRAFORM (официальная документация): + // «If an attribute value is configured, it is NEVER valid to change that + // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что + // написал пользователь). Менять plan запрещено на уровне фреймворка. + // + // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре. + // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный). + // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит: + // «Provider produced inconsistent result after apply». + // + // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот. + // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как + // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API). + // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный + // регистр в state через strings.EqualFold (сравниваем без учёта регистра). + // ══════════════════════════════════════════════════════════════════════════ + originalKafkaUid := data.KafkaUid + if !data.KafkaUid.IsNull() && !data.KafkaUid.IsUnknown() { + resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, data.KafkaUid.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if resolvedKafkaUid != "" && resolvedKafkaUid != data.KafkaUid.ValueString() { + data.KafkaUid = types.StringValue(resolvedKafkaUid) + } + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + if !data.Domain.IsNull() && !data.Domain.IsUnknown() { + desiredDomain = data.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 119, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 550: resources_core.FormatString(data.ResourceRealm), + 551: resources_core.FormatInt64(data.ResourceMemory), + 552: resources_core.FormatInt64(data.ResourceCPU), + 553: resources_core.FormatString(data.Domain), + 554: resources_core.FormatInt64(data.ResourceInstances), + 555: resources_core.FormatString(data.KafkaUid), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 119, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 119, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "kafkaUid", Field: "KafkaUid", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() { + resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, state.KafkaUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve kafka_uid", err.Error()) + } else if resolvedKafkaUid != "" && resolvedKafkaUid != state.KafkaUid.ValueString() { + state.KafkaUid = types.StringValue(resolvedKafkaUid) + } + } + // Restore user-provided casing в state (Create). + // EqualFold = «совпадают ли значения без учёта регистра?» + // Если да — значит API вернул «туже» строку, только в другом регистре. + // Заменяем state на original (то что было в plan/config пользователя). + // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint. + if !originalKafkaUid.IsNull() && !originalKafkaUid.IsUnknown() && !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() { + if strings.EqualFold(state.KafkaUid.ValueString(), originalKafkaUid.ValueString()) { + state.KafkaUid = originalKafkaUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *AkhqResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state AkhqModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 119, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "kafkaUid", Field: "KafkaUid", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !newState.KafkaUid.IsNull() && !newState.KafkaUid.IsUnknown() { + resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, newState.KafkaUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve kafka_uid", err.Error()) + } else if resolvedKafkaUid != "" && resolvedKafkaUid != newState.KafkaUid.ValueString() { + newState.KafkaUid = types.StringValue(resolvedKafkaUid) + } + } + // Restore user-provided casing в state (Read). + // При чтении у нас нет plan — но предыдущий state уже хранит значение + // в регистре пользователя (после первого Create оно было восстановлено). + // Берём prior state (переменная state) как эталон регистра. + // Если API вернул то же UUID только строчными буквами — восстанавливаем. + if !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() && !newState.KafkaUid.IsNull() && !newState.KafkaUid.IsUnknown() { + if strings.EqualFold(newState.KafkaUid.ValueString(), state.KafkaUid.ValueString()) { + newState.KafkaUid = state.KafkaUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *AkhqResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan AkhqModel + var state AkhqModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.KafkaUid.IsNull() != state.KafkaUid.IsNull() || plan.KafkaUid.IsUnknown() != state.KafkaUid.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.KafkaUid.IsNull() && !plan.KafkaUid.IsUnknown() && !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() { + if !strings.EqualFold(plan.KafkaUid.ValueString(), state.KafkaUid.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 565: resources_core.FormatString(plan.KafkaUid), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 119, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "kafkaUid", Field: "KafkaUid", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() { + resolvedKafkaUid, err := r.client.ResolveRefSvcParamValue(ctx, 116, state.KafkaUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve kafka_uid", err.Error()) + } else if resolvedKafkaUid != "" && resolvedKafkaUid != state.KafkaUid.ValueString() { + state.KafkaUid = types.StringValue(resolvedKafkaUid) + } + } + // Restore user-provided casing в state (Update). + // После API-вызова modify state содержит значения в lower-case от API. + // Plan == config == то что написал пользователь (регистр неизменён). + // EqualFold: если UUID совпадает без учёта регистра — берём из plan. + if !plan.KafkaUid.IsNull() && !plan.KafkaUid.IsUnknown() && !state.KafkaUid.IsNull() && !state.KafkaUid.IsUnknown() { + if strings.EqualFold(state.KafkaUid.ValueString(), plan.KafkaUid.ValueString()) { + state.KafkaUid = plan.KafkaUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *AkhqResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state AkhqModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *AkhqResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *AkhqResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/120_clickhouse_database_resource.go b/generated/test/go/120_clickhouse_database_resource.go new file mode 100644 index 0000000..cc5c42e --- /dev/null +++ b/generated/test/go/120_clickhouse_database_resource.go @@ -0,0 +1,343 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: clickhouse +// Subresource: database + +var _ resource.Resource = &ClickhouseDatabaseResource{} + +// ClickhouseDatabaseResource manages a subresource via operations. +type ClickhouseDatabaseResource struct { + client *core.UniversalClient +} + +type ClickhouseDatabaseModel struct { + ID types.String `tfsdk:"id"` + ClickhouseID types.String `tfsdk:"clickhouse_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + DbName types.String `tfsdk:"db_name"` +} + +func NewClickhouseDatabaseResource() resource.Resource { + return &ClickhouseDatabaseResource{} +} + +func (r *ClickhouseDatabaseResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_clickhouse_database" +} + +func (r *ClickhouseDatabaseResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "clickhouse_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "db_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("db01"), MarkdownDescription: "Пример: `db01` Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *ClickhouseDatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan ClickhouseDatabaseModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.ClickhouseID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "dbName": resources_core.FormatString(plan.DbName), + } + identityCodes := []string{ + "dbName", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("database", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "dbName": resources_core.FormatString(plan.DbName), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_database", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_database", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *ClickhouseDatabaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state ClickhouseDatabaseModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *ClickhouseDatabaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan ClickhouseDatabaseModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state ClickhouseDatabaseModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.ClickhouseID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{ + "dbName": resources_core.FormatString(plan.DbName), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *ClickhouseDatabaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state ClickhouseDatabaseModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_database" == "" { + return + } + + instanceUID := strings.TrimSpace(state.ClickhouseID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "dbName": resources_core.FormatString(state.DbName), + } + identityCodes := []string{ + "dbName", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("database", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "dbName": resources_core.FormatString(state.DbName), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_database", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *ClickhouseDatabaseResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/120_clickhouse_resource.go b/generated/test/go/120_clickhouse_resource.go new file mode 100644 index 0000000..b5c560d --- /dev/null +++ b/generated/test/go/120_clickhouse_resource.go @@ -0,0 +1,487 @@ +package resources_gen + +import ( + "context" + "fmt" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: clickhouse +// Service ID: 120 + +var _ resource.Resource = &ClickhouseResource{} +var _ resource.ResourceWithModifyPlan = &ClickhouseResource{} +var _ resource.ResourceWithImportState = &ClickhouseResource{} + +type ClickhouseResource struct { + client *core.UniversalClient +} + +// ClickhouseAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration. +type ClickhouseAccessConfigurationModel struct { + MasterIpSpace types.String `tfsdk:"master_ip_space" json:"masterIpSpace"` + MasterAccessList types.String `tfsdk:"master_access_list" json:"masterAccessList"` +} + +// ClickhouseAutoscaleConfigurationModel — вложенная модель для map-fixed параметра autoscaleConfiguration. +type ClickhouseAutoscaleConfigurationModel struct { + Enabled types.Bool `tfsdk:"enabled" json:"enabled"` + Schedule types.Int64 `tfsdk:"schedule" json:"schedule"` + Quota types.Int64 `tfsdk:"quota" json:"quota"` + Percent types.Int64 `tfsdk:"percent" json:"percent"` +} + +// ClickhouseBackupConfigurationModel — вложенная модель для map-fixed параметра backupConfiguration. +type ClickhouseBackupConfigurationModel struct { + Retain types.Int64 `tfsdk:"retain" json:"retain"` + Schedule types.String `tfsdk:"schedule" json:"schedule"` + S3Uid types.String `tfsdk:"s3_uid" json:"s3Uid"` +} + +// ClickhouseClickhouseConfigurationModel — вложенная модель для map-fixed параметра clickhouseConfiguration. +type ClickhouseClickhouseConfigurationModel struct { + Version types.String `tfsdk:"version" json:"version"` +} + +// ClickhouseClickhouseKeeperConfigurationModel — вложенная модель для map-fixed параметра clickhouseKeeperConfiguration. +type ClickhouseClickhouseKeeperConfigurationModel struct { + Cpu types.Int64 `tfsdk:"cpu" json:"cpu"` + Memory types.Int64 `tfsdk:"memory" json:"memory"` + Replicas types.Int64 `tfsdk:"replicas" json:"replicas"` +} + +// ClickhouseClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type ClickhouseClusterConfigurationModel struct { + Cpu types.Int64 `tfsdk:"cpu" json:"cpu"` + Memory types.Int64 `tfsdk:"memory" json:"memory"` + Replicas types.Int64 `tfsdk:"replicas" json:"replicas"` + Shards types.Int64 `tfsdk:"shards" json:"shards"` + Disk types.Int64 `tfsdk:"disk" json:"disk"` +} + +// ClickhouseStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type ClickhouseStartupConfigurationModel struct { + ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"` +} + +type ClickhouseModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessConfiguration *ClickhouseAccessConfigurationModel `tfsdk:"access_configuration"` + AutoscaleConfiguration *ClickhouseAutoscaleConfigurationModel `tfsdk:"autoscale_configuration"` + BackupConfiguration *ClickhouseBackupConfigurationModel `tfsdk:"backup_configuration"` + ClickhouseConfiguration *ClickhouseClickhouseConfigurationModel `tfsdk:"clickhouse_configuration"` + ClickhouseFilesConfiguration types.String `tfsdk:"clickhouse_files_configuration"` + ClickhouseKeeperConfiguration *ClickhouseClickhouseKeeperConfigurationModel `tfsdk:"clickhouse_keeper_configuration"` + ClusterConfiguration *ClickhouseClusterConfigurationModel `tfsdk:"cluster_configuration"` + StartupConfiguration *ClickhouseStartupConfigurationModel `tfsdk:"startup_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewClickhouseResource() resource.Resource { + return &ClickhouseResource{} +} + +func (r *ClickhouseResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_clickhouse" +} + +func (r *ClickhouseResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "master_ip_space": schema.StringAttribute{Required: true, MarkdownDescription: "Из какого Ip-Space резервировать внешний IP
Список предоставляется из Организации, в котором развёрнута ресурсная платформа"}, + "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"}, + }}, + "autoscale_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "enabled": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включает автоскейлинг PV. При включении параметра необходимо настроить autoScalePercentage и autoScaleTechWindow. Алерт при срабатывании вызывает modify у текущей услуги"}, + "schedule": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(0), MarkdownDescription: "Требует включения autoScale. Окно обновления (пока не реализовано). Указывается час в виде Integer (0 / 5 / 12 / 23)"}, + "quota": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100), MarkdownDescription: "До какого размера возможно увеличивать диск, когда включен Autoscale"}, + "percent": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Требует включения autoScale. Позволяет указать в процентах расширение от текущего макс объема
Минимальный размер на который расширяется - **1Gb**"}, + }}, + "backup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "retain": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(7)}, + "schedule": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("0 0 * * *")}, + "s3_uid": schema.StringAttribute{Required: true, MarkdownDescription: "Экземпляр S3 обязателен"}, + }}, + "clickhouse_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("25.10")}, + }}, + "clickhouse_files_configuration": schema.StringAttribute{Optional: true, MarkdownDescription: "Формат:
- path: somethings3.xml
xmlConfig: '<clickhouse>......</clickhouse>'"}, + "clickhouse_keeper_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"}, + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Указывается в Megabytes"}, + "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(0), MarkdownDescription: "Если указан 0, то инстансы ClickHouse Keeper развёрнуты не будут"}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1000), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"}, + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2048), MarkdownDescription: "Указывается в Megabytes"}, + "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)}, + "shards": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "**Шард** - это часть данных, разделенная на разные узлы (серверы) для горизонтального масштабирования и обработки больших объемов информации.
**Реплика** - это точная копия данных внутри одного шарда"}, + "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Указывается в GIGAbytes"}, + }}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"}, + }}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *ClickhouseResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *ClickhouseModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *ClickhouseModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan ClickhouseModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 837: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}), + 838: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "shards": fmt.Sprintf("%d", config.ClusterConfiguration.Shards.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64())}), + 839: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterAccessList.ValueString())}), + 840: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClickhouseKeeperConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClickhouseKeeperConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClickhouseKeeperConfiguration.Replicas.ValueInt64())}), + 841: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.ClickhouseConfiguration.Version.ValueString())}), + 842: resources_core.BuildJSON(map[string]string{"retain": fmt.Sprintf("%d", config.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", config.BackupConfiguration.Schedule.ValueString()), "s3Uid": fmt.Sprintf("\"%s\"", config.BackupConfiguration.S3Uid.ValueString())}), + 843: resources_core.BuildJSON(map[string]string{"enabled": fmt.Sprintf("%v", config.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", config.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", config.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", config.AutoscaleConfiguration.Percent.ValueInt64())}), + 851: resources_core.FormatString(config.ClickhouseFilesConfiguration), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 120, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *ClickhouseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data ClickhouseModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 120, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 837: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}), + 838: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "shards": fmt.Sprintf("%d", data.ClusterConfiguration.Shards.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64())}), + 839: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterAccessList.ValueString())}), + 840: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClickhouseKeeperConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClickhouseKeeperConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClickhouseKeeperConfiguration.Replicas.ValueInt64())}), + 841: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.ClickhouseConfiguration.Version.ValueString())}), + 842: resources_core.BuildJSON(map[string]string{"retain": fmt.Sprintf("%d", data.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", data.BackupConfiguration.Schedule.ValueString()), "s3Uid": fmt.Sprintf("\"%s\"", data.BackupConfiguration.S3Uid.ValueString())}), + 843: resources_core.BuildJSON(map[string]string{"enabled": fmt.Sprintf("%v", data.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", data.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", data.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", data.AutoscaleConfiguration.Percent.ValueInt64())}), + 851: resources_core.FormatString(data.ClickhouseFilesConfiguration), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 120, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 120, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"}, + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"}, + {Code: "clickhouseConfiguration", Field: "ClickhouseConfiguration", Type: "map-fixed"}, + {Code: "clickhouseFilesConfiguration", Field: "ClickhouseFilesConfiguration", Type: "array-map-fixed"}, + {Code: "clickhouseKeeperConfiguration", Field: "ClickhouseKeeperConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *ClickhouseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state ClickhouseModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 120, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"}, + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"}, + {Code: "clickhouseConfiguration", Field: "ClickhouseConfiguration", Type: "map-fixed"}, + {Code: "clickhouseFilesConfiguration", Field: "ClickhouseFilesConfiguration", Type: "array-map-fixed"}, + {Code: "clickhouseKeeperConfiguration", Field: "ClickhouseKeeperConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *ClickhouseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan ClickhouseModel + var state ClickhouseModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.ClickhouseFilesConfiguration.IsNull() != state.ClickhouseFilesConfiguration.IsNull() || plan.ClickhouseFilesConfiguration.IsUnknown() != state.ClickhouseFilesConfiguration.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ClickhouseFilesConfiguration.IsNull() && !plan.ClickhouseFilesConfiguration.IsUnknown() && !state.ClickhouseFilesConfiguration.IsNull() && !state.ClickhouseFilesConfiguration.IsUnknown() { + if !strings.EqualFold(plan.ClickhouseFilesConfiguration.ValueString(), state.ClickhouseFilesConfiguration.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 844: resources_core.BuildJSON(map[string]string{"shards": fmt.Sprintf("%d", plan.ClusterConfiguration.Shards.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64())}), + 845: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClickhouseKeeperConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClickhouseKeeperConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClickhouseKeeperConfiguration.Replicas.ValueInt64())}), + 846: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterAccessList.ValueString())}), + 847: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", plan.ClickhouseConfiguration.Version.ValueString())}), + 848: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", plan.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", plan.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", plan.BackupConfiguration.Schedule.ValueString())}), + 849: resources_core.BuildJSON(map[string]string{"enabled": fmt.Sprintf("%v", plan.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Percent.ValueInt64())}), + 850: resources_core.FormatString(plan.ClickhouseFilesConfiguration), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 120, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"}, + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"}, + {Code: "clickhouseConfiguration", Field: "ClickhouseConfiguration", Type: "map-fixed"}, + {Code: "clickhouseFilesConfiguration", Field: "ClickhouseFilesConfiguration", Type: "array-map-fixed"}, + {Code: "clickhouseKeeperConfiguration", Field: "ClickhouseKeeperConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *ClickhouseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state ClickhouseModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *ClickhouseResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *ClickhouseResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/120_clickhouse_user_resource.go b/generated/test/go/120_clickhouse_user_resource.go new file mode 100644 index 0000000..8dc28f7 --- /dev/null +++ b/generated/test/go/120_clickhouse_user_resource.go @@ -0,0 +1,359 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: clickhouse +// Subresource: user + +var _ resource.Resource = &ClickhouseUserResource{} + +// ClickhouseUserResource manages a subresource via operations. +type ClickhouseUserResource struct { + client *core.UniversalClient +} + +type ClickhouseUserModel struct { + ID types.String `tfsdk:"id"` + ClickhouseID types.String `tfsdk:"clickhouse_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessHosts types.String `tfsdk:"access_hosts"` + DbName types.String `tfsdk:"db_name"` + Username types.String `tfsdk:"username"` +} + +func NewClickhouseUserResource() resource.Resource { + return &ClickhouseUserResource{} +} + +func (r *ClickhouseUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_clickhouse_user" +} + +func (r *ClickhouseUserResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "clickhouse_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_hosts": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("::/0"), MarkdownDescription: "Пример: `::/0` Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "db_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "username": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("myusername"), MarkdownDescription: "Пример: `myusername` Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *ClickhouseUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan ClickhouseUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.ClickhouseID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "accessHosts": resources_core.FormatString(plan.AccessHosts), + "dbName": resources_core.FormatString(plan.DbName), + "username": resources_core.FormatString(plan.Username), + } + identityCodes := []string{ + "accessHosts", + "dbName", + "username", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "username": resources_core.FormatString(plan.Username), + "dbName": resources_core.FormatString(plan.DbName), + "accessHosts": resources_core.FormatString(plan.AccessHosts), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *ClickhouseUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state ClickhouseUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *ClickhouseUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan ClickhouseUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state ClickhouseUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.ClickhouseID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{ + "accessHosts": resources_core.FormatString(plan.AccessHosts), + "dbName": resources_core.FormatString(plan.DbName), + "username": resources_core.FormatString(plan.Username), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *ClickhouseUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state ClickhouseUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_user" == "" { + return + } + + instanceUID := strings.TrimSpace(state.ClickhouseID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "accessHosts": resources_core.FormatString(state.AccessHosts), + "dbName": resources_core.FormatString(state.DbName), + "username": resources_core.FormatString(state.Username), + } + identityCodes := []string{ + "accessHosts", + "dbName", + "username", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "username": resources_core.FormatString(state.Username), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_user", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *ClickhouseUserResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/12_s3_resource.go b/generated/test/go/12_s3_resource.go new file mode 100644 index 0000000..2d179e4 --- /dev/null +++ b/generated/test/go/12_s3_resource.go @@ -0,0 +1,421 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: s3 +// Service ID: 12 + +var _ resource.Resource = &S3Resource{} +var _ resource.ResourceWithModifyPlan = &S3Resource{} +var _ resource.ResourceWithImportState = &S3Resource{} + +type S3Resource struct { + client *core.UniversalClient +} + +type S3Model struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + DisplayName types.String `tfsdk:"display_name"` + MaxBucketsPerUser types.Int64 `tfsdk:"max_buckets_per_user"` + MaxObjectsPerBucket types.Int64 `tfsdk:"max_objects_per_bucket"` + MaxSizeGbPerUser types.Int64 `tfsdk:"max_size_gb_per_user"` + ResourceRealm types.String `tfsdk:"resource_realm"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewS3Resource() resource.Resource { + return &S3Resource{} +} + +func (r *S3Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_s3" +} + +func (r *S3Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "display_name": schema.StringAttribute{Required: true, MarkdownDescription: "Человеко-читаемое имя (транслит из Elma) - передаётся имя клиента Не изменяется после создания."}, + "max_buckets_per_user": schema.Int64Attribute{Optional: true, MarkdownDescription: "Квота на пользователя. Количество бакетов пользователя. Передаём руками"}, + "max_objects_per_bucket": schema.Int64Attribute{Optional: true, MarkdownDescription: "Квота на бакет. Кол-во объектов. Передаём руками"}, + "max_size_gb_per_user": schema.Int64Attribute{Optional: true, MarkdownDescription: "maxSizeGb. Передаём руками"}, + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("ceph.tst.nubes.ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *S3Resource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *S3Model + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *S3Model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan S3Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.DisplayName.IsNull() && !plan.DisplayName.IsUnknown() && !state.DisplayName.IsNull() && !state.DisplayName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.DisplayName.ValueString(), state.DisplayName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить display_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.DisplayName.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "display_name is required.") + return + } + if config.DisplayName.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 163: resources_core.FormatString(config.ResourceRealm), + 320: resources_core.FormatString(config.DisplayName), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 12, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *S3Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data S3Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.DisplayName.IsNull() || data.DisplayName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "display_name is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 12, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 163: resources_core.FormatString(data.ResourceRealm), + 320: resources_core.FormatString(data.DisplayName), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 12, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 12, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "displayName", Field: "DisplayName", Type: "string"}, + {Code: "maxBucketsPerUser", Field: "MaxBucketsPerUser", Type: "int64"}, + {Code: "maxObjectsPerBucket", Field: "MaxObjectsPerBucket", Type: "int64"}, + {Code: "maxSizeGbPerUser", Field: "MaxSizeGbPerUser", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *S3Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state S3Model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 12, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "displayName", Field: "DisplayName", Type: "string"}, + {Code: "maxBucketsPerUser", Field: "MaxBucketsPerUser", Type: "int64"}, + {Code: "maxObjectsPerBucket", Field: "MaxObjectsPerBucket", Type: "int64"}, + {Code: "maxSizeGbPerUser", Field: "MaxSizeGbPerUser", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *S3Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan S3Model + var state S3Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.MaxSizeGbPerUser.IsNull() != state.MaxSizeGbPerUser.IsNull() || plan.MaxSizeGbPerUser.IsUnknown() != state.MaxSizeGbPerUser.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.MaxSizeGbPerUser.IsNull() && !plan.MaxSizeGbPerUser.IsUnknown() && !state.MaxSizeGbPerUser.IsNull() && !state.MaxSizeGbPerUser.IsUnknown() { + if plan.MaxSizeGbPerUser.ValueInt64() != state.MaxSizeGbPerUser.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.MaxObjectsPerBucket.IsNull() != state.MaxObjectsPerBucket.IsNull() || plan.MaxObjectsPerBucket.IsUnknown() != state.MaxObjectsPerBucket.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.MaxObjectsPerBucket.IsNull() && !plan.MaxObjectsPerBucket.IsUnknown() && !state.MaxObjectsPerBucket.IsNull() && !state.MaxObjectsPerBucket.IsUnknown() { + if plan.MaxObjectsPerBucket.ValueInt64() != state.MaxObjectsPerBucket.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.MaxBucketsPerUser.IsNull() != state.MaxBucketsPerUser.IsNull() || plan.MaxBucketsPerUser.IsUnknown() != state.MaxBucketsPerUser.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.MaxBucketsPerUser.IsNull() && !plan.MaxBucketsPerUser.IsUnknown() && !state.MaxBucketsPerUser.IsNull() && !state.MaxBucketsPerUser.IsUnknown() { + if plan.MaxBucketsPerUser.ValueInt64() != state.MaxBucketsPerUser.ValueInt64() { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 50: resources_core.FormatInt64(plan.MaxSizeGbPerUser), + 51: resources_core.FormatInt64(plan.MaxObjectsPerBucket), + 52: resources_core.FormatInt64(plan.MaxBucketsPerUser), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 12, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "displayName", Field: "DisplayName", Type: "string"}, + {Code: "maxBucketsPerUser", Field: "MaxBucketsPerUser", Type: "int64"}, + {Code: "maxObjectsPerBucket", Field: "MaxObjectsPerBucket", Type: "int64"}, + {Code: "maxSizeGbPerUser", Field: "MaxSizeGbPerUser", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *S3Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state S3Model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *S3Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *S3Resource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/12_s3_sub_user_resource.go b/generated/test/go/12_s3_sub_user_resource.go new file mode 100644 index 0000000..b8e0168 --- /dev/null +++ b/generated/test/go/12_s3_sub_user_resource.go @@ -0,0 +1,358 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: s3 +// Subresource: sub_user + +var _ resource.Resource = &S3SubUserResource{} + +// S3SubUserResource manages a subresource via operations. +type S3SubUserResource struct { + client *core.UniversalClient +} + +type S3SubUserModel struct { + ID types.String `tfsdk:"id"` + S3ID types.String `tfsdk:"s3_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + SubUserAccess types.String `tfsdk:"sub_user_access"` + SubUserName types.String `tfsdk:"sub_user_name"` +} + +func NewS3SubUserResource() resource.Resource { + return &S3SubUserResource{} +} + +func (r *S3SubUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_s3_sub_user" +} + +func (r *S3SubUserResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "s3_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "sub_user_access": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "sub_user_name": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *S3SubUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan S3SubUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if plan.SubUserAccess.IsNull() || plan.SubUserAccess.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "sub_user_access is required.") + return + } + if plan.SubUserName.IsNull() || plan.SubUserName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "sub_user_name is required.") + return + } + + instanceUID := strings.TrimSpace(plan.S3ID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "subUserAccess": resources_core.FormatString(plan.SubUserAccess), + "subUserName": resources_core.FormatString(plan.SubUserName), + } + identityCodes := []string{ + "subUserAccess", + "subUserName", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("sub_user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "sub_user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "subUserName": resources_core.FormatString(plan.SubUserName), + "subUserAccess": resources_core.FormatString(plan.SubUserAccess), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_sub_user", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_sub_user", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "sub_user", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "sub_user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "sub_user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *S3SubUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state S3SubUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *S3SubUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan S3SubUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state S3SubUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.S3ID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{ + "subUserAccess": resources_core.FormatString(plan.SubUserAccess), + "subUserName": resources_core.FormatString(plan.SubUserName), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "sub_user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *S3SubUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state S3SubUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_sub_user" == "" { + return + } + + instanceUID := strings.TrimSpace(state.S3ID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "subUserAccess": resources_core.FormatString(state.SubUserAccess), + "subUserName": resources_core.FormatString(state.SubUserName), + } + identityCodes := []string{ + "subUserAccess", + "subUserName", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("sub_user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "subUserName": resources_core.FormatString(state.SubUserName), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_sub_user", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *S3SubUserResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/13_s3bucket_resource.go b/generated/test/go/13_s3bucket_resource.go new file mode 100644 index 0000000..644ffb7 --- /dev/null +++ b/generated/test/go/13_s3bucket_resource.go @@ -0,0 +1,478 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: s3bucket +// Service ID: 13 + +var _ resource.Resource = &S3bucketResource{} +var _ resource.ResourceWithModifyPlan = &S3bucketResource{} +var _ resource.ResourceWithImportState = &S3bucketResource{} + +type S3bucketResource struct { + client *core.UniversalClient +} + +type S3bucketModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + BucketName types.String `tfsdk:"bucket_name"` + CorsAll types.Bool `tfsdk:"cors_all"` + ListAll types.Bool `tfsdk:"list_all"` + MaxSize types.String `tfsdk:"max_size"` + Placement types.String `tfsdk:"placement"` + ReadAll types.Bool `tfsdk:"read_all"` + S3UserUid types.String `tfsdk:"s3_user_uid"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewS3bucketResource() resource.Resource { + return &S3bucketResource{} +} + +func (r *S3bucketResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_s3bucket" +} + +func (r *S3bucketResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "bucket_name": schema.StringAttribute{Required: true, MarkdownDescription: "Имя бакета s3 Не изменяется после создания."}, + "cors_all": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Публичный доступ к чтению настроек позволяет любому пользователю интернета читать конфигурации CORS Не изменяется после создания."}, + "list_all": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Публичный доступ к листингу позволяет любому пользователю интернета получить список объектов в бакете. Не изменяется после создания."}, + "max_size": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("-1"), MarkdownDescription: "Максимальный размер бакета s3. Если поставить -1, будет безлимит. Размер указывается в Bytes Не изменяется после создания."}, + "placement": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("HOT"), MarkdownDescription: "Класс хранилища, который используется по умолчанию при загрузке объектов. Чем холоднее, тем дешевле место, но дороже обращения Не изменяется после создания."}, + "read_all": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Публичный доступ на чтение позволяет любому пользователю интернета скачивать объекты из бакета. Не изменяется после создания."}, + "s3_user_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UID Корневой услуги S3. На основе него будет создан бакет. Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *S3bucketResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *S3bucketModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *S3bucketModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan S3bucketModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan. + // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов. + // Resolve (displayName → UUID или uppercase → lowercase) нужен только для + // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка + // "Provider produced invalid plan: planned value does not match config value". + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.BucketName.IsNull() && !plan.BucketName.IsUnknown() && !state.BucketName.IsNull() && !state.BucketName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.BucketName.ValueString(), state.BucketName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить bucket_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.CorsAll.IsNull() && !plan.CorsAll.IsUnknown() && !state.CorsAll.IsNull() && !state.CorsAll.IsUnknown() { + if plan.CorsAll.ValueBool() != state.CorsAll.ValueBool() { + resp.Diagnostics.AddError("Нельзя изменить cors_all", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ListAll.IsNull() && !plan.ListAll.IsUnknown() && !state.ListAll.IsNull() && !state.ListAll.IsUnknown() { + if plan.ListAll.ValueBool() != state.ListAll.ValueBool() { + resp.Diagnostics.AddError("Нельзя изменить list_all", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.MaxSize.IsNull() && !plan.MaxSize.IsUnknown() && !state.MaxSize.IsNull() && !state.MaxSize.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.MaxSize.ValueString(), state.MaxSize.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить max_size", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.Placement.IsNull() && !plan.Placement.IsUnknown() && !state.Placement.IsNull() && !state.Placement.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Placement.ValueString(), state.Placement.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить placement", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ReadAll.IsNull() && !plan.ReadAll.IsUnknown() && !state.ReadAll.IsNull() && !state.ReadAll.IsUnknown() { + if plan.ReadAll.ValueBool() != state.ReadAll.ValueBool() { + resp.Diagnostics.AddError("Нельзя изменить read_all", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.S3UserUid.IsNull() && !plan.S3UserUid.IsUnknown() && !state.S3UserUid.IsNull() && !state.S3UserUid.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.S3UserUid.ValueString(), state.S3UserUid.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить s3_user_uid", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.BucketName.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "bucket_name is required.") + return + } + if config.BucketName.IsUnknown() { + return + } + if config.S3UserUid.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "s3_user_uid is required.") + return + } + if config.S3UserUid.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + if !config.S3UserUid.IsNull() && !config.S3UserUid.IsUnknown() { + resolvedS3UserUid, err := r.client.ResolveRefSvcParamValue(ctx, 12, config.S3UserUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve s3_user_uid", err.Error()) + } else if resolvedS3UserUid != "" && resolvedS3UserUid != config.S3UserUid.ValueString() { + config.S3UserUid = types.StringValue(resolvedS3UserUid) + } + } + params := map[int]string{ + 124: resources_core.FormatString(config.S3UserUid), + 125: resources_core.FormatString(config.BucketName), + 126: resources_core.FormatString(config.MaxSize), + 127: resources_core.FormatBool(config.ReadAll), + 128: resources_core.FormatBool(config.ListAll), + 129: resources_core.FormatBool(config.CorsAll), + 130: resources_core.FormatString(config.Placement), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 13, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *S3bucketResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data S3bucketModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.BucketName.IsNull() || data.BucketName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "bucket_name is required.") + return + } + if data.S3UserUid.IsNull() || data.S3UserUid.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "s3_user_uid is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 13, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 124: resources_core.FormatString(data.S3UserUid), + 125: resources_core.FormatString(data.BucketName), + 126: resources_core.FormatString(data.MaxSize), + 127: resources_core.FormatBool(data.ReadAll), + 128: resources_core.FormatBool(data.ListAll), + 129: resources_core.FormatBool(data.CorsAll), + 130: resources_core.FormatString(data.Placement), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 13, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 13, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "bucketName", Field: "BucketName", Type: "string"}, + {Code: "corsAll", Field: "CorsAll", Type: "bool"}, + {Code: "listAll", Field: "ListAll", Type: "bool"}, + {Code: "maxSize", Field: "MaxSize", Type: "string"}, + {Code: "placement", Field: "Placement", Type: "string"}, + {Code: "readAll", Field: "ReadAll", Type: "bool"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + // Restore user-provided casing в state (Create). + // EqualFold = «совпадают ли значения без учёта регистра?» + // Если да — значит API вернул «туже» строку, только в другом регистре. + // Заменяем state на original (то что было в plan/config пользователя). + // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint. + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *S3bucketResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state S3bucketModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 13, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "bucketName", Field: "BucketName", Type: "string"}, + {Code: "corsAll", Field: "CorsAll", Type: "bool"}, + {Code: "listAll", Field: "ListAll", Type: "bool"}, + {Code: "maxSize", Field: "MaxSize", Type: "string"}, + {Code: "placement", Field: "Placement", Type: "string"}, + {Code: "readAll", Field: "ReadAll", Type: "bool"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + // Restore user-provided casing в state (Read). + // При чтении у нас нет plan — но предыдущий state уже хранит значение + // в регистре пользователя (после первого Create оно было восстановлено). + // Берём prior state (переменная state) как эталон регистра. + // Если API вернул то же UUID только строчными буквами — восстанавливаем. + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *S3bucketResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan S3bucketModel + var state S3bucketModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 13, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "bucketName", Field: "BucketName", Type: "string"}, + {Code: "corsAll", Field: "CorsAll", Type: "bool"}, + {Code: "listAll", Field: "ListAll", Type: "bool"}, + {Code: "maxSize", Field: "MaxSize", Type: "string"}, + {Code: "placement", Field: "Placement", Type: "string"}, + {Code: "readAll", Field: "ReadAll", Type: "bool"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + // Restore user-provided casing в state (Update). + // После API-вызова modify state содержит значения в lower-case от API. + // Plan == config == то что написал пользователь (регистр неизменён). + // EqualFold: если UUID совпадает без учёта регистра — берём из plan. + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *S3bucketResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state S3bucketModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *S3bucketResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *S3bucketResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/148_vc_mgmt_sthutrval_cluster_resource.go b/generated/test/go/148_vc_mgmt_sthutrval_cluster_resource.go new file mode 100644 index 0000000..cfd5712 --- /dev/null +++ b/generated/test/go/148_vc_mgmt_sthutrval_cluster_resource.go @@ -0,0 +1,419 @@ +package resources_gen + +import ( + "context" + "fmt" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vc_mgmt_sthutrval_cluster +// Service ID: 148 + +var _ resource.Resource = &VcMgmtSthutrvalClusterResource{} +var _ resource.ResourceWithModifyPlan = &VcMgmtSthutrvalClusterResource{} +var _ resource.ResourceWithImportState = &VcMgmtSthutrvalClusterResource{} + +type VcMgmtSthutrvalClusterResource struct { + client *core.UniversalClient +} + +// VcMgmtSthutrvalClusterAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration. +type VcMgmtSthutrvalClusterAccessConfigurationModel struct { + NeedExternalAddressAPI types.Bool `tfsdk:"need_external_address_a_p_i" json:"needExternalAddressAPI"` + AccessIpListAPI types.String `tfsdk:"access_ip_list_a_p_i" json:"accessIpListAPI"` + NeedExternalAddressIngress types.Bool `tfsdk:"need_external_address_ingress" json:"needExternalAddressIngress"` + AccessIpListIngress types.String `tfsdk:"access_ip_list_ingress" json:"accessIpListIngress"` +} + +// VcMgmtSthutrvalClusterClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type VcMgmtSthutrvalClusterClusterConfigurationModel struct { + AppVersion types.String `tfsdk:"app_version" json:"appVersion"` +} + +// VcMgmtSthutrvalClusterControlPlaneConfigurationModel — вложенная модель для map-fixed параметра controlPlaneConfiguration. +type VcMgmtSthutrvalClusterControlPlaneConfigurationModel struct { + SizingPolicy types.String `tfsdk:"sizing_policy" json:"sizingPolicy"` + SizingDisk types.Int64 `tfsdk:"sizing_disk" json:"sizingDisk"` + Count types.Int64 `tfsdk:"count" json:"count"` +} + +// VcMgmtSthutrvalClusterStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type VcMgmtSthutrvalClusterStartupConfigurationModel struct { + ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"` + ClusterName types.String `tfsdk:"cluster_name" json:"clusterName"` +} + +type VcMgmtSthutrvalClusterModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessConfiguration *VcMgmtSthutrvalClusterAccessConfigurationModel `tfsdk:"access_configuration"` + ClusterConfiguration *VcMgmtSthutrvalClusterClusterConfigurationModel `tfsdk:"cluster_configuration"` + ControlPlaneConfiguration *VcMgmtSthutrvalClusterControlPlaneConfigurationModel `tfsdk:"control_plane_configuration"` + StartupConfiguration *VcMgmtSthutrvalClusterStartupConfigurationModel `tfsdk:"startup_configuration"` + WorkerConfiguration types.String `tfsdk:"worker_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewVcMgmtSthutrvalClusterResource() resource.Resource { + return &VcMgmtSthutrvalClusterResource{} +} + +func (r *VcMgmtSthutrvalClusterResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vc_mgmt_sthutrval_cluster" +} + +func (r *VcMgmtSthutrvalClusterResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "need_external_address_a_p_i": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Выделяет внешний IP для доступа к kubernetes API. Пока нельзя выставить false"}, + "access_ip_list_a_p_i": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Access IP List для подключения к Kubernetes API. Указывается массив. Если передан пустой массив, доступ выделяется всем
Пример: **["0.0.0.0/0"]** или **[]** или **["178.4.13.2", "178.4.14.0/24"]**"}, + "need_external_address_ingress": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Выделяет внешний IP для доступа к Ingress"}, + "access_ip_list_ingress": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Access IP List для подключения к Ingress. Указывается массив. Если передан пустой массив, доступ выделяется всем
Пример: **["0.0.0.0/0"]** или **[]** или **["178.4.13.2", "178.4.14.0/24"]**"}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "app_version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("2.13.1")}, + }}, + "control_plane_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "sizing_policy": schema.StringAttribute{Required: true, MarkdownDescription: "Доступные sizing Policy представлены исходя из ресурсной платформы, которая была выбрана"}, + "sizing_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(50)}, + "count": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)}, + }}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("sandbox.nubes.ru"), MarkdownDescription: "Облако в котором будет развёрнут тенант"}, + "cluster_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("example-co5wmalx")}, + }}, + "worker_configuration": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VcMgmtSthutrvalClusterResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *VcMgmtSthutrvalClusterModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *VcMgmtSthutrvalClusterModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan VcMgmtSthutrvalClusterModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.WorkerConfiguration.IsNull() && !plan.WorkerConfiguration.IsUnknown() && !state.WorkerConfiguration.IsNull() && !state.WorkerConfiguration.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.WorkerConfiguration.ValueString(), state.WorkerConfiguration.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить worker_configuration", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 771: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString()), "clusterName": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ClusterName.ValueString())}), + 772: resources_core.BuildJSON(map[string]string{"appVersion": fmt.Sprintf("\"%s\"", config.ClusterConfiguration.AppVersion.ValueString())}), + 773: resources_core.BuildJSON(map[string]string{"sizingPolicy": fmt.Sprintf("\"%s\"", config.ControlPlaneConfiguration.SizingPolicy.ValueString()), "sizingDisk": fmt.Sprintf("%d", config.ControlPlaneConfiguration.SizingDisk.ValueInt64()), "count": fmt.Sprintf("%d", config.ControlPlaneConfiguration.Count.ValueInt64())}), + 774: resources_core.FormatString(config.WorkerConfiguration), + 775: resources_core.BuildJSON(map[string]string{"needExternalAddressAPI": fmt.Sprintf("%v", config.AccessConfiguration.NeedExternalAddressAPI.ValueBool()), "accessIpListAPI": fmt.Sprintf("\"%s\"", config.AccessConfiguration.AccessIpListAPI.ValueString()), "needExternalAddressIngress": fmt.Sprintf("%v", config.AccessConfiguration.NeedExternalAddressIngress.ValueBool()), "accessIpListIngress": fmt.Sprintf("\"%s\"", config.AccessConfiguration.AccessIpListIngress.ValueString())}), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 148, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *VcMgmtSthutrvalClusterResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data VcMgmtSthutrvalClusterModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 148, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 771: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString()), "clusterName": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ClusterName.ValueString())}), + 772: resources_core.BuildJSON(map[string]string{"appVersion": fmt.Sprintf("\"%s\"", data.ClusterConfiguration.AppVersion.ValueString())}), + 773: resources_core.BuildJSON(map[string]string{"sizingPolicy": fmt.Sprintf("\"%s\"", data.ControlPlaneConfiguration.SizingPolicy.ValueString()), "sizingDisk": fmt.Sprintf("%d", data.ControlPlaneConfiguration.SizingDisk.ValueInt64()), "count": fmt.Sprintf("%d", data.ControlPlaneConfiguration.Count.ValueInt64())}), + 774: resources_core.FormatString(data.WorkerConfiguration), + 775: resources_core.BuildJSON(map[string]string{"needExternalAddressAPI": fmt.Sprintf("%v", data.AccessConfiguration.NeedExternalAddressAPI.ValueBool()), "accessIpListAPI": fmt.Sprintf("\"%s\"", data.AccessConfiguration.AccessIpListAPI.ValueString()), "needExternalAddressIngress": fmt.Sprintf("%v", data.AccessConfiguration.NeedExternalAddressIngress.ValueBool()), "accessIpListIngress": fmt.Sprintf("\"%s\"", data.AccessConfiguration.AccessIpListIngress.ValueString())}), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 148, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 148, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "controlPlaneConfiguration", Field: "ControlPlaneConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + {Code: "workerConfiguration", Field: "WorkerConfiguration", Type: "array-map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcMgmtSthutrvalClusterResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VcMgmtSthutrvalClusterModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 148, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "controlPlaneConfiguration", Field: "ControlPlaneConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + {Code: "workerConfiguration", Field: "WorkerConfiguration", Type: "array-map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *VcMgmtSthutrvalClusterResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VcMgmtSthutrvalClusterModel + var state VcMgmtSthutrvalClusterModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 148, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "controlPlaneConfiguration", Field: "ControlPlaneConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + {Code: "workerConfiguration", Field: "WorkerConfiguration", Type: "array-map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcMgmtSthutrvalClusterResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VcMgmtSthutrvalClusterModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *VcMgmtSthutrvalClusterResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *VcMgmtSthutrvalClusterResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/149_valo_tenant_resource.go b/generated/test/go/149_valo_tenant_resource.go new file mode 100644 index 0000000..252f012 --- /dev/null +++ b/generated/test/go/149_valo_tenant_resource.go @@ -0,0 +1,431 @@ +package resources_gen + +import ( + "context" + "fmt" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: valo_tenant +// Service ID: 149 + +var _ resource.Resource = &ValoTenantResource{} +var _ resource.ResourceWithModifyPlan = &ValoTenantResource{} +var _ resource.ResourceWithImportState = &ValoTenantResource{} + +type ValoTenantResource struct { + client *core.UniversalClient +} + +// ValoTenantValoOptionsModel — вложенная модель для map-fixed параметра valoOptions. +type ValoTenantValoOptionsModel struct { + RequestApproveMaxPeriod types.Int64 `tfsdk:"request_approve_max_period" json:"requestApproveMaxPeriod"` + FfOnlineEditor types.Bool `tfsdk:"ff_online_editor" json:"ffOnlineEditor"` + FfWatermark types.Bool `tfsdk:"ff_watermark" json:"ffWatermark"` + FfUserImport types.Bool `tfsdk:"ff_user_import" json:"ffUserImport"` + FfThemes types.Bool `tfsdk:"ff_themes" json:"ffThemes"` + FfPublicLink types.Bool `tfsdk:"ff_public_link" json:"ffPublicLink"` + FfLogging types.Bool `tfsdk:"ff_logging" json:"ffLogging"` + UserCount types.Int64 `tfsdk:"user_count" json:"userCount"` +} + +type ValoTenantModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + Domain types.String `tfsdk:"domain"` + ResourceRealm types.String `tfsdk:"resource_realm"` + S3Storage types.Int64 `tfsdk:"s3_storage"` + ValoOptions *ValoTenantValoOptionsModel `tfsdk:"valo_options"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewValoTenantResource() resource.Resource { + return &ValoTenantResource{} +} + +func (r *ValoTenantResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_valo_tenant" +} + +func (r *ValoTenantResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "domain": schema.StringAttribute{Required: true, MarkdownDescription: "Будет добавлено в зону valo.ngcloud.ru Не изменяется после создания."}, + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("valo.ngcloud.ru"), MarkdownDescription: "Не изменяется после создания."}, + "s3_storage": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Параметр указывается в `Gb`"}, + "valo_options": schema.SingleNestedAttribute{Optional: true, Computed: true, + Attributes: map[string]schema.Attribute{ + "request_approve_max_period": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2), MarkdownDescription: "Максимальное значение периода согласования (в днях)"}, + "ff_online_editor": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Тарифная опция"}, + "ff_watermark": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Тарифная опция"}, + "ff_user_import": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Тарифная опция"}, + "ff_themes": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Тарифная опция"}, + "ff_public_link": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Тарифная опция"}, + "ff_logging": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Тарифная опция"}, + "user_count": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10)}, + }}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *ValoTenantResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *ValoTenantModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *ValoTenantModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan ValoTenantModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.Domain.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "domain is required.") + return + } + if config.Domain.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 655: resources_core.FormatString(config.ResourceRealm), + 656: resources_core.FormatString(config.Domain), + 657: resources_core.BuildJSON(map[string]string{"requestApproveMaxPeriod": fmt.Sprintf("%d", config.ValoOptions.RequestApproveMaxPeriod.ValueInt64()), "ffOnlineEditor": fmt.Sprintf("%v", config.ValoOptions.FfOnlineEditor.ValueBool()), "ffWatermark": fmt.Sprintf("%v", config.ValoOptions.FfWatermark.ValueBool()), "ffUserImport": fmt.Sprintf("%v", config.ValoOptions.FfUserImport.ValueBool()), "ffThemes": fmt.Sprintf("%v", config.ValoOptions.FfThemes.ValueBool()), "ffPublicLink": fmt.Sprintf("%v", config.ValoOptions.FfPublicLink.ValueBool()), "ffLogging": fmt.Sprintf("%v", config.ValoOptions.FfLogging.ValueBool()), "userCount": fmt.Sprintf("%d", config.ValoOptions.UserCount.ValueInt64())}), + 658: resources_core.FormatInt64(config.S3Storage), + } + desiredDomain := "" + if !config.Domain.IsNull() && !config.Domain.IsUnknown() { + desiredDomain = config.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 149, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *ValoTenantResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data ValoTenantModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.Domain.IsNull() || data.Domain.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "domain is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + if !data.Domain.IsNull() && !data.Domain.IsUnknown() { + desiredDomain = data.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 149, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 655: resources_core.FormatString(data.ResourceRealm), + 656: resources_core.FormatString(data.Domain), + 657: resources_core.BuildJSON(map[string]string{"requestApproveMaxPeriod": fmt.Sprintf("%d", data.ValoOptions.RequestApproveMaxPeriod.ValueInt64()), "ffOnlineEditor": fmt.Sprintf("%v", data.ValoOptions.FfOnlineEditor.ValueBool()), "ffWatermark": fmt.Sprintf("%v", data.ValoOptions.FfWatermark.ValueBool()), "ffUserImport": fmt.Sprintf("%v", data.ValoOptions.FfUserImport.ValueBool()), "ffThemes": fmt.Sprintf("%v", data.ValoOptions.FfThemes.ValueBool()), "ffPublicLink": fmt.Sprintf("%v", data.ValoOptions.FfPublicLink.ValueBool()), "ffLogging": fmt.Sprintf("%v", data.ValoOptions.FfLogging.ValueBool()), "userCount": fmt.Sprintf("%d", data.ValoOptions.UserCount.ValueInt64())}), + 658: resources_core.FormatInt64(data.S3Storage), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 149, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 149, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "s3Storage", Field: "S3Storage", Type: "int64"}, + {Code: "valoOptions", Field: "ValoOptions", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *ValoTenantResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state ValoTenantModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 149, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "s3Storage", Field: "S3Storage", Type: "int64"}, + {Code: "valoOptions", Field: "ValoOptions", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *ValoTenantResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan ValoTenantModel + var state ValoTenantModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.S3Storage.IsNull() != state.S3Storage.IsNull() || plan.S3Storage.IsUnknown() != state.S3Storage.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.S3Storage.IsNull() && !plan.S3Storage.IsUnknown() && !state.S3Storage.IsNull() && !state.S3Storage.IsUnknown() { + if plan.S3Storage.ValueInt64() != state.S3Storage.ValueInt64() { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 659: resources_core.BuildJSON(map[string]string{"ffWatermark": fmt.Sprintf("%v", plan.ValoOptions.FfWatermark.ValueBool()), "ffUserImport": fmt.Sprintf("%v", plan.ValoOptions.FfUserImport.ValueBool()), "ffThemes": fmt.Sprintf("%v", plan.ValoOptions.FfThemes.ValueBool()), "ffPublicLink": fmt.Sprintf("%v", plan.ValoOptions.FfPublicLink.ValueBool()), "ffLogging": fmt.Sprintf("%v", plan.ValoOptions.FfLogging.ValueBool()), "userCount": fmt.Sprintf("%d", plan.ValoOptions.UserCount.ValueInt64()), "requestApproveMaxPeriod": fmt.Sprintf("%d", plan.ValoOptions.RequestApproveMaxPeriod.ValueInt64()), "ffOnlineEditor": fmt.Sprintf("%v", plan.ValoOptions.FfOnlineEditor.ValueBool())}), + 660: resources_core.FormatInt64(plan.S3Storage), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 149, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "s3Storage", Field: "S3Storage", Type: "int64"}, + {Code: "valoOptions", Field: "ValoOptions", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *ValoTenantResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state ValoTenantModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *ValoTenantResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *ValoTenantResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/150_k8s_sthutrval_cluster_resource.go b/generated/test/go/150_k8s_sthutrval_cluster_resource.go new file mode 100644 index 0000000..d77d291 --- /dev/null +++ b/generated/test/go/150_k8s_sthutrval_cluster_resource.go @@ -0,0 +1,429 @@ +package resources_gen + +import ( + "context" + "fmt" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: k8s_sthutrval_cluster +// Service ID: 150 + +var _ resource.Resource = &K8sSthutrvalClusterResource{} +var _ resource.ResourceWithModifyPlan = &K8sSthutrvalClusterResource{} +var _ resource.ResourceWithImportState = &K8sSthutrvalClusterResource{} + +type K8sSthutrvalClusterResource struct { + client *core.UniversalClient +} + +// K8sSthutrvalClusterAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration. +type K8sSthutrvalClusterAccessConfigurationModel struct { + NeedExternalAddressAPI types.Bool `tfsdk:"need_external_address_a_p_i" json:"needExternalAddressAPI"` + AccessIpListAPI types.String `tfsdk:"access_ip_list_a_p_i" json:"accessIpListAPI"` + NeedExternalAddressIngress types.Bool `tfsdk:"need_external_address_ingress" json:"needExternalAddressIngress"` + AccessIpListIngress types.String `tfsdk:"access_ip_list_ingress" json:"accessIpListIngress"` +} + +// K8sSthutrvalClusterClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type K8sSthutrvalClusterClusterConfigurationModel struct { + AppVersion types.String `tfsdk:"app_version" json:"appVersion"` +} + +// K8sSthutrvalClusterControlPlaneConfigurationModel — вложенная модель для map-fixed параметра controlPlaneConfiguration. +type K8sSthutrvalClusterControlPlaneConfigurationModel struct { + SizingPolicy types.String `tfsdk:"sizing_policy" json:"sizingPolicy"` + SizingDisk types.Int64 `tfsdk:"sizing_disk" json:"sizingDisk"` + Count types.Int64 `tfsdk:"count" json:"count"` +} + +// K8sSthutrvalClusterStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type K8sSthutrvalClusterStartupConfigurationModel struct { + NsxtUid types.String `tfsdk:"nsxt_uid" json:"nsxtUid"` + ClusterName types.String `tfsdk:"cluster_name" json:"clusterName"` + VdcUid types.String `tfsdk:"vdc_uid" json:"vdcUid"` +} + +type K8sSthutrvalClusterModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessConfiguration *K8sSthutrvalClusterAccessConfigurationModel `tfsdk:"access_configuration"` + ClusterConfiguration *K8sSthutrvalClusterClusterConfigurationModel `tfsdk:"cluster_configuration"` + ControlPlaneConfiguration *K8sSthutrvalClusterControlPlaneConfigurationModel `tfsdk:"control_plane_configuration"` + StartupConfiguration *K8sSthutrvalClusterStartupConfigurationModel `tfsdk:"startup_configuration"` + WorkerConfiguration types.String `tfsdk:"worker_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewK8sSthutrvalClusterResource() resource.Resource { + return &K8sSthutrvalClusterResource{} +} + +func (r *K8sSthutrvalClusterResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_k8s_sthutrval_cluster" +} + +func (r *K8sSthutrvalClusterResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "need_external_address_a_p_i": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Выделяет внешний IP для доступа к kubernetes API. Пока нельзя выставить false"}, + "access_ip_list_a_p_i": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Access IP List для подключения к Kubernetes API. Указывается массив. Если передан пустой массив, доступ выделяется всем"}, + "need_external_address_ingress": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Выделяет внешний IP для доступа к Ingress"}, + "access_ip_list_ingress": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Access IP List для подключения к Ingress. Указывается массив. Если передан пустой массив, доступ выделяется всем"}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "app_version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("2.13.1")}, + }}, + "control_plane_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "sizing_policy": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Доступные sizing Policy можно обновлять через услугу `Виртуальный датацентр`"}, + "sizing_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(50), MarkdownDescription: "Дисковая политика берётся из услуги VDC, первый в списке"}, + "count": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)}, + }}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "nsxt_uid": schema.StringAttribute{Required: true}, + "cluster_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("example-biu4ghxt")}, + "vdc_uid": schema.StringAttribute{Required: true}, + }}, + "worker_configuration": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]")}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *K8sSthutrvalClusterResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *K8sSthutrvalClusterModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *K8sSthutrvalClusterModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan K8sSthutrvalClusterModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 750: resources_core.BuildJSON(map[string]string{"nsxtUid": fmt.Sprintf("\"%s\"", config.StartupConfiguration.NsxtUid.ValueString()), "clusterName": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ClusterName.ValueString()), "vdcUid": fmt.Sprintf("\"%s\"", config.StartupConfiguration.VdcUid.ValueString())}), + 751: resources_core.BuildJSON(map[string]string{"sizingPolicy": fmt.Sprintf("\"%s\"", config.ControlPlaneConfiguration.SizingPolicy.ValueString()), "sizingDisk": fmt.Sprintf("%d", config.ControlPlaneConfiguration.SizingDisk.ValueInt64()), "count": fmt.Sprintf("%d", config.ControlPlaneConfiguration.Count.ValueInt64())}), + 752: resources_core.FormatString(config.WorkerConfiguration), + 753: resources_core.BuildJSON(map[string]string{"needExternalAddressAPI": fmt.Sprintf("%v", config.AccessConfiguration.NeedExternalAddressAPI.ValueBool()), "accessIpListAPI": fmt.Sprintf("\"%s\"", config.AccessConfiguration.AccessIpListAPI.ValueString()), "needExternalAddressIngress": fmt.Sprintf("%v", config.AccessConfiguration.NeedExternalAddressIngress.ValueBool()), "accessIpListIngress": fmt.Sprintf("\"%s\"", config.AccessConfiguration.AccessIpListIngress.ValueString())}), + 755: resources_core.BuildJSON(map[string]string{"appVersion": fmt.Sprintf("\"%s\"", config.ClusterConfiguration.AppVersion.ValueString())}), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 150, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *K8sSthutrvalClusterResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data K8sSthutrvalClusterModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 150, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 750: resources_core.BuildJSON(map[string]string{"nsxtUid": fmt.Sprintf("\"%s\"", data.StartupConfiguration.NsxtUid.ValueString()), "clusterName": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ClusterName.ValueString()), "vdcUid": fmt.Sprintf("\"%s\"", data.StartupConfiguration.VdcUid.ValueString())}), + 751: resources_core.BuildJSON(map[string]string{"sizingPolicy": fmt.Sprintf("\"%s\"", data.ControlPlaneConfiguration.SizingPolicy.ValueString()), "sizingDisk": fmt.Sprintf("%d", data.ControlPlaneConfiguration.SizingDisk.ValueInt64()), "count": fmt.Sprintf("%d", data.ControlPlaneConfiguration.Count.ValueInt64())}), + 752: resources_core.FormatString(data.WorkerConfiguration), + 753: resources_core.BuildJSON(map[string]string{"needExternalAddressAPI": fmt.Sprintf("%v", data.AccessConfiguration.NeedExternalAddressAPI.ValueBool()), "accessIpListAPI": fmt.Sprintf("\"%s\"", data.AccessConfiguration.AccessIpListAPI.ValueString()), "needExternalAddressIngress": fmt.Sprintf("%v", data.AccessConfiguration.NeedExternalAddressIngress.ValueBool()), "accessIpListIngress": fmt.Sprintf("\"%s\"", data.AccessConfiguration.AccessIpListIngress.ValueString())}), + 755: resources_core.BuildJSON(map[string]string{"appVersion": fmt.Sprintf("\"%s\"", data.ClusterConfiguration.AppVersion.ValueString())}), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 150, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 150, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "controlPlaneConfiguration", Field: "ControlPlaneConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + {Code: "workerConfiguration", Field: "WorkerConfiguration", Type: "array-map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *K8sSthutrvalClusterResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state K8sSthutrvalClusterModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 150, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "controlPlaneConfiguration", Field: "ControlPlaneConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + {Code: "workerConfiguration", Field: "WorkerConfiguration", Type: "array-map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *K8sSthutrvalClusterResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan K8sSthutrvalClusterModel + var state K8sSthutrvalClusterModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.WorkerConfiguration.IsNull() != state.WorkerConfiguration.IsNull() || plan.WorkerConfiguration.IsUnknown() != state.WorkerConfiguration.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.WorkerConfiguration.IsNull() && !plan.WorkerConfiguration.IsUnknown() && !state.WorkerConfiguration.IsNull() && !state.WorkerConfiguration.IsUnknown() { + if !strings.EqualFold(plan.WorkerConfiguration.ValueString(), state.WorkerConfiguration.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 756: resources_core.BuildJSON(map[string]string{"appVersion": fmt.Sprintf("\"%s\"", plan.ClusterConfiguration.AppVersion.ValueString())}), + 757: resources_core.BuildJSON(map[string]string{"sizingPolicy": fmt.Sprintf("\"%s\"", plan.ControlPlaneConfiguration.SizingPolicy.ValueString()), "sizingDisk": fmt.Sprintf("%d", plan.ControlPlaneConfiguration.SizingDisk.ValueInt64()), "count": fmt.Sprintf("%d", plan.ControlPlaneConfiguration.Count.ValueInt64())}), + 758: resources_core.FormatString(plan.WorkerConfiguration), + 759: resources_core.BuildJSON(map[string]string{"accessIpListIngress": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.AccessIpListIngress.ValueString()), "needExternalAddressAPI": fmt.Sprintf("%v", plan.AccessConfiguration.NeedExternalAddressAPI.ValueBool()), "accessIpListAPI": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.AccessIpListAPI.ValueString()), "needExternalAddressIngress": fmt.Sprintf("%v", plan.AccessConfiguration.NeedExternalAddressIngress.ValueBool())}), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 150, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "controlPlaneConfiguration", Field: "ControlPlaneConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + {Code: "workerConfiguration", Field: "WorkerConfiguration", Type: "array-map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *K8sSthutrvalClusterResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state K8sSthutrvalClusterModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *K8sSthutrvalClusterResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *K8sSthutrvalClusterResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/150_k8s_sthutrval_cluster_user_resource.go b/generated/test/go/150_k8s_sthutrval_cluster_user_resource.go new file mode 100644 index 0000000..55faf0e --- /dev/null +++ b/generated/test/go/150_k8s_sthutrval_cluster_user_resource.go @@ -0,0 +1,343 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: k8s_sthutrval_cluster +// Subresource: user + +var _ resource.Resource = &K8sSthutrvalClusterUserResource{} + +// K8sSthutrvalClusterUserResource manages a subresource via operations. +type K8sSthutrvalClusterUserResource struct { + client *core.UniversalClient +} + +type K8sSthutrvalClusterUserModel struct { + ID types.String `tfsdk:"id"` + K8sSthutrvalClusterID types.String `tfsdk:"k8s_sthutrval_cluster_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + Admins types.String `tfsdk:"admins"` +} + +func NewK8sSthutrvalClusterUserResource() resource.Resource { + return &K8sSthutrvalClusterUserResource{} +} + +func (r *K8sSthutrvalClusterUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_k8s_sthutrval_cluster_user" +} + +func (r *K8sSthutrvalClusterUserResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "k8s_sthutrval_cluster_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "admins": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *K8sSthutrvalClusterUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan K8sSthutrvalClusterUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.K8sSthutrvalClusterID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "admins": resources_core.FormatString(plan.Admins), + } + identityCodes := []string{ + "admins", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "admins": resources_core.FormatString(plan.Admins), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *K8sSthutrvalClusterUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state K8sSthutrvalClusterUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *K8sSthutrvalClusterUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan K8sSthutrvalClusterUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state K8sSthutrvalClusterUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.K8sSthutrvalClusterID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{ + "admins": resources_core.FormatString(plan.Admins), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *K8sSthutrvalClusterUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state K8sSthutrvalClusterUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_user" == "" { + return + } + + instanceUID := strings.TrimSpace(state.K8sSthutrvalClusterID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "admins": resources_core.FormatString(state.Admins), + } + identityCodes := []string{ + "admins", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "admins": resources_core.FormatString(state.Admins), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_user", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *K8sSthutrvalClusterUserResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/151_k8s_openbao_kv_resource.go b/generated/test/go/151_k8s_openbao_kv_resource.go new file mode 100644 index 0000000..df5550e --- /dev/null +++ b/generated/test/go/151_k8s_openbao_kv_resource.go @@ -0,0 +1,343 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: k8s_openbao +// Subresource: kv + +var _ resource.Resource = &K8sOpenbaoKvResource{} + +// K8sOpenbaoKvResource manages a subresource via operations. +type K8sOpenbaoKvResource struct { + client *core.UniversalClient +} + +type K8sOpenbaoKvModel struct { + ID types.String `tfsdk:"id"` + K8sOpenbaoID types.String `tfsdk:"k8s_openbao_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + KvName types.String `tfsdk:"kv_name"` +} + +func NewK8sOpenbaoKvResource() resource.Resource { + return &K8sOpenbaoKvResource{} +} + +func (r *K8sOpenbaoKvResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_k8s_openbao_kv" +} + +func (r *K8sOpenbaoKvResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "k8s_openbao_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "kv_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("kvname-yyz2zm23"), MarkdownDescription: "Будет создан `v2 тип движка`

Уникальное в рамках услуги Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *K8sOpenbaoKvResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan K8sOpenbaoKvModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.K8sOpenbaoID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "kvName": resources_core.FormatString(plan.KvName), + } + identityCodes := []string{ + "kvName", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("kv", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "kv", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "kvName": resources_core.FormatString(plan.KvName), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_kv", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_kv", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "kv", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "kv", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "kv", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *K8sOpenbaoKvResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state K8sOpenbaoKvModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *K8sOpenbaoKvResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan K8sOpenbaoKvModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state K8sOpenbaoKvModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.K8sOpenbaoID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{ + "kvName": resources_core.FormatString(plan.KvName), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "kv", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *K8sOpenbaoKvResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state K8sOpenbaoKvModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_kv" == "" { + return + } + + instanceUID := strings.TrimSpace(state.K8sOpenbaoID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "kvName": resources_core.FormatString(state.KvName), + } + identityCodes := []string{ + "kvName", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("kv", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "kvName": resources_core.FormatString(state.KvName), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_kv", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *K8sOpenbaoKvResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/151_k8s_openbao_policy_resource.go b/generated/test/go/151_k8s_openbao_policy_resource.go new file mode 100644 index 0000000..ef23e6d --- /dev/null +++ b/generated/test/go/151_k8s_openbao_policy_resource.go @@ -0,0 +1,355 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: k8s_openbao +// Subresource: policy + +var _ resource.Resource = &K8sOpenbaoPolicyResource{} + +// K8sOpenbaoPolicyResource manages a subresource via operations. +type K8sOpenbaoPolicyResource struct { + client *core.UniversalClient +} + +type K8sOpenbaoPolicyModel struct { + ID types.String `tfsdk:"id"` + K8sOpenbaoID types.String `tfsdk:"k8s_openbao_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + PolicyName types.String `tfsdk:"policy_name"` + PolicySettings types.String `tfsdk:"policy_settings"` +} + +func NewK8sOpenbaoPolicyResource() resource.Resource { + return &K8sOpenbaoPolicyResource{} +} + +func (r *K8sOpenbaoPolicyResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_k8s_openbao_policy" +} + +func (r *K8sOpenbaoPolicyResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "k8s_openbao_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "policy_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("policy-r2ksc6sl"), MarkdownDescription: "Сделано для дальнейшего удобного использования Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "policy_settings": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *K8sOpenbaoPolicyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan K8sOpenbaoPolicyModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if plan.PolicySettings.IsNull() || plan.PolicySettings.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "policy_settings is required.") + return + } + + instanceUID := strings.TrimSpace(plan.K8sOpenbaoID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "policyName": resources_core.FormatString(plan.PolicyName), + "policySettings": resources_core.FormatString(plan.PolicySettings), + } + identityCodes := []string{ + "policyName", + "policySettings", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("policy", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "policy", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "policySettings": resources_core.FormatString(plan.PolicySettings), + "policyName": resources_core.FormatString(plan.PolicyName), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_policy", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_policy", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "policy", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "policy", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "policy", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *K8sOpenbaoPolicyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state K8sOpenbaoPolicyModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *K8sOpenbaoPolicyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan K8sOpenbaoPolicyModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state K8sOpenbaoPolicyModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.K8sOpenbaoID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{ + "policyName": resources_core.FormatString(plan.PolicyName), + "policySettings": resources_core.FormatString(plan.PolicySettings), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "policy", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *K8sOpenbaoPolicyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state K8sOpenbaoPolicyModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_policy" == "" { + return + } + + instanceUID := strings.TrimSpace(state.K8sOpenbaoID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "policyName": resources_core.FormatString(state.PolicyName), + "policySettings": resources_core.FormatString(state.PolicySettings), + } + identityCodes := []string{ + "policyName", + "policySettings", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("policy", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "policyName": resources_core.FormatString(state.PolicyName), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_policy", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *K8sOpenbaoPolicyResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/151_k8s_openbao_resource.go b/generated/test/go/151_k8s_openbao_resource.go new file mode 100644 index 0000000..92dcaf2 --- /dev/null +++ b/generated/test/go/151_k8s_openbao_resource.go @@ -0,0 +1,468 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: k8s_openbao +// Service ID: 151 + +var _ resource.Resource = &K8sOpenbaoResource{} +var _ resource.ResourceWithModifyPlan = &K8sOpenbaoResource{} +var _ resource.ResourceWithImportState = &K8sOpenbaoResource{} + +type K8sOpenbaoResource struct { + client *core.UniversalClient +} + +type K8sOpenbaoModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AppVersion types.String `tfsdk:"app_version"` + Domain types.String `tfsdk:"domain"` + ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"` + ResourceDisk types.Int64 `tfsdk:"resource_disk"` + ResourceInstances types.Int64 `tfsdk:"resource_instances"` + ResourceMemory types.Int64 `tfsdk:"resource_memory"` + ResourceRealm types.String `tfsdk:"resource_realm"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewK8sOpenbaoResource() resource.Resource { + return &K8sOpenbaoResource{} +} + +func (r *K8sOpenbaoResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_k8s_openbao" +} + +func (r *K8sOpenbaoResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "app_version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("2.5.2"), MarkdownDescription: "Выбрать из списка"}, + "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("bao-uqvjbvor"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."}, + "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Пример: `500`"}, + "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Пример: `10`"}, + "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(3), MarkdownDescription: "Пример: `3`"}, + "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Пример: `2048`"}, + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes в котором будет развёрнуто приложение Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *K8sOpenbaoResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *K8sOpenbaoModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *K8sOpenbaoModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan K8sOpenbaoModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.ResourceRealm.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + return + } + if config.ResourceRealm.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 663: resources_core.FormatString(config.ResourceRealm), + 664: resources_core.FormatInt64(config.ResourceCPU), + 665: resources_core.FormatInt64(config.ResourceInstances), + 666: resources_core.FormatInt64(config.ResourceMemory), + 667: resources_core.FormatInt64(config.ResourceDisk), + 668: resources_core.FormatString(config.AppVersion), + 669: resources_core.FormatString(config.Domain), + } + desiredDomain := "" + if !config.Domain.IsNull() && !config.Domain.IsUnknown() { + desiredDomain = config.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 151, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *K8sOpenbaoResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data K8sOpenbaoModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + if !data.Domain.IsNull() && !data.Domain.IsUnknown() { + desiredDomain = data.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 151, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 663: resources_core.FormatString(data.ResourceRealm), + 664: resources_core.FormatInt64(data.ResourceCPU), + 665: resources_core.FormatInt64(data.ResourceInstances), + 666: resources_core.FormatInt64(data.ResourceMemory), + 667: resources_core.FormatInt64(data.ResourceDisk), + 668: resources_core.FormatString(data.AppVersion), + 669: resources_core.FormatString(data.Domain), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 151, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 151, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "appVersion", Field: "AppVersion", Type: "string"}, + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *K8sOpenbaoResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state K8sOpenbaoModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 151, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "appVersion", Field: "AppVersion", Type: "string"}, + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *K8sOpenbaoResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan K8sOpenbaoModel + var state K8sOpenbaoModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.ResourceInstances.IsNull() != state.ResourceInstances.IsNull() || plan.ResourceInstances.IsUnknown() != state.ResourceInstances.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() { + if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceCPU.IsNull() != state.ResourceCPU.IsNull() || plan.ResourceCPU.IsUnknown() != state.ResourceCPU.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() { + if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceMemory.IsNull() != state.ResourceMemory.IsNull() || plan.ResourceMemory.IsUnknown() != state.ResourceMemory.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() { + if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceDisk.IsNull() != state.ResourceDisk.IsNull() || plan.ResourceDisk.IsUnknown() != state.ResourceDisk.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() { + if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.AppVersion.IsNull() != state.AppVersion.IsNull() || plan.AppVersion.IsUnknown() != state.AppVersion.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.AppVersion.IsNull() && !plan.AppVersion.IsUnknown() && !state.AppVersion.IsNull() && !state.AppVersion.IsUnknown() { + if !strings.EqualFold(plan.AppVersion.ValueString(), state.AppVersion.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 670: resources_core.FormatInt64(plan.ResourceInstances), + 671: resources_core.FormatInt64(plan.ResourceCPU), + 672: resources_core.FormatInt64(plan.ResourceMemory), + 673: resources_core.FormatInt64(plan.ResourceDisk), + 674: resources_core.FormatString(plan.AppVersion), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 151, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "appVersion", Field: "AppVersion", Type: "string"}, + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *K8sOpenbaoResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state K8sOpenbaoModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *K8sOpenbaoResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *K8sOpenbaoResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/151_k8s_openbao_secrets_resource.go b/generated/test/go/151_k8s_openbao_secrets_resource.go new file mode 100644 index 0000000..161164e --- /dev/null +++ b/generated/test/go/151_k8s_openbao_secrets_resource.go @@ -0,0 +1,368 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: k8s_openbao +// Subresource: secrets + +var _ resource.Resource = &K8sOpenbaoSecretsResource{} + +// K8sOpenbaoSecretsResource manages a subresource via operations. +type K8sOpenbaoSecretsResource struct { + client *core.UniversalClient +} + +type K8sOpenbaoSecretsModel struct { + ID types.String `tfsdk:"id"` + K8sOpenbaoID types.String `tfsdk:"k8s_openbao_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + KvName types.String `tfsdk:"kv_name"` + SecretData types.String `tfsdk:"secret_data"` + SecretPath types.String `tfsdk:"secret_path"` +} + +func NewK8sOpenbaoSecretsResource() resource.Resource { + return &K8sOpenbaoSecretsResource{} +} + +func (r *K8sOpenbaoSecretsResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_k8s_openbao_secrets" +} + +func (r *K8sOpenbaoSecretsResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "k8s_openbao_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "kv_name": schema.StringAttribute{Required: true, MarkdownDescription: "Выберите из списка Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "secret_data": schema.StringAttribute{Required: true, MarkdownDescription: "Хотя бы один секрет необходимо добавить Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "secret_path": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("path/to/mysecret"), MarkdownDescription: "Пример: `path` или `road/to/path` Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *K8sOpenbaoSecretsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan K8sOpenbaoSecretsModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if plan.KvName.IsNull() || plan.KvName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "kv_name is required.") + return + } + if plan.SecretData.IsNull() || plan.SecretData.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "secret_data is required.") + return + } + + instanceUID := strings.TrimSpace(plan.K8sOpenbaoID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "kvName": resources_core.FormatString(plan.KvName), + "secretData": resources_core.FormatString(plan.SecretData), + "secretPath": resources_core.FormatString(plan.SecretPath), + } + identityCodes := []string{ + "kvName", + "secretData", + "secretPath", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("secrets", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "secrets", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "kvName": resources_core.FormatString(plan.KvName), + "secretPath": resources_core.FormatString(plan.SecretPath), + "secretData": resources_core.FormatString(plan.SecretData), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_secrets", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_secrets", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "secrets", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "secrets", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "secrets", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *K8sOpenbaoSecretsResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state K8sOpenbaoSecretsModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *K8sOpenbaoSecretsResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan K8sOpenbaoSecretsModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state K8sOpenbaoSecretsModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.K8sOpenbaoID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{ + "kvName": resources_core.FormatString(plan.KvName), + "secretData": resources_core.FormatString(plan.SecretData), + "secretPath": resources_core.FormatString(plan.SecretPath), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "secrets", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *K8sOpenbaoSecretsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state K8sOpenbaoSecretsModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_secrets" == "" { + return + } + + instanceUID := strings.TrimSpace(state.K8sOpenbaoID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "kvName": resources_core.FormatString(state.KvName), + "secretData": resources_core.FormatString(state.SecretData), + "secretPath": resources_core.FormatString(state.SecretPath), + } + identityCodes := []string{ + "kvName", + "secretData", + "secretPath", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("secrets", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "kvName": resources_core.FormatString(state.KvName), + "secretPath": resources_core.FormatString(state.SecretPath), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_secrets", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *K8sOpenbaoSecretsResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/163_llm_ai_resource.go b/generated/test/go/163_llm_ai_resource.go new file mode 100644 index 0000000..1037328 --- /dev/null +++ b/generated/test/go/163_llm_ai_resource.go @@ -0,0 +1,345 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: llm_ai +// Service ID: 163 + +var _ resource.Resource = &LlmAiResource{} +var _ resource.ResourceWithModifyPlan = &LlmAiResource{} +var _ resource.ResourceWithImportState = &LlmAiResource{} + +type LlmAiResource struct { + client *core.UniversalClient +} + +type LlmAiModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + ResourceRealm types.String `tfsdk:"resource_realm"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewLlmAiResource() resource.Resource { + return &LlmAiResource{} +} + +func (r *LlmAiResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_llm_ai" +} + +func (r *LlmAiResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("llmAi.aillm.ru"), MarkdownDescription: "Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *LlmAiResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *LlmAiModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *LlmAiModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan LlmAiModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 826: resources_core.FormatString(config.ResourceRealm), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 163, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *LlmAiResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data LlmAiModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 163, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 826: resources_core.FormatString(data.ResourceRealm), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 163, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 163, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *LlmAiResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state LlmAiModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 163, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *LlmAiResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan LlmAiModel + var state LlmAiModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 163, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *LlmAiResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state LlmAiModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *LlmAiResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *LlmAiResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/163_llm_ai_secrets_resource.go b/generated/test/go/163_llm_ai_secrets_resource.go new file mode 100644 index 0000000..063f1fb --- /dev/null +++ b/generated/test/go/163_llm_ai_secrets_resource.go @@ -0,0 +1,389 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: llm_ai +// Subresource: secrets + +var _ resource.Resource = &LlmAiSecretsResource{} + +// LlmAiSecretsResource manages a subresource via operations. +type LlmAiSecretsResource struct { + client *core.UniversalClient +} + +type LlmAiSecretsModel struct { + ID types.String `tfsdk:"id"` + LlmAiID types.String `tfsdk:"llm_ai_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + KeyName types.String `tfsdk:"key_name"` + Models types.String `tfsdk:"models"` + RpmLimit types.Int64 `tfsdk:"rpm_limit"` + TeamName types.String `tfsdk:"team_name"` + TpmLimit types.Int64 `tfsdk:"tpm_limit"` +} + +func NewLlmAiSecretsResource() resource.Resource { + return &LlmAiSecretsResource{} +} + +func (r *LlmAiSecretsResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_llm_ai_secrets" +} + +func (r *LlmAiSecretsResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "llm_ai_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "key_name": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "models": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "rpm_limit": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(60), MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.Int64{int64planmodifier.RequiresReplace()}}, + "team_name": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "tpm_limit": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100000), MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.Int64{int64planmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *LlmAiSecretsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan LlmAiSecretsModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if plan.KeyName.IsNull() || plan.KeyName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "key_name is required.") + return + } + if plan.Models.IsNull() || plan.Models.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "models is required.") + return + } + if plan.TeamName.IsNull() || plan.TeamName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "team_name is required.") + return + } + + instanceUID := strings.TrimSpace(plan.LlmAiID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "keyName": resources_core.FormatString(plan.KeyName), + "models": resources_core.FormatString(plan.Models), + "rpmLimit": resources_core.FormatInt64(plan.RpmLimit), + "teamName": resources_core.FormatString(plan.TeamName), + "tpmLimit": resources_core.FormatInt64(plan.TpmLimit), + } + identityCodes := []string{ + "keyName", + "models", + "rpmLimit", + "teamName", + "tpmLimit", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("secrets", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "secrets", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "models": resources_core.FormatString(plan.Models), + "rpmLimit": resources_core.FormatInt64(plan.RpmLimit), + "tpmLimit": resources_core.FormatInt64(plan.TpmLimit), + "keyName": resources_core.FormatString(plan.KeyName), + "teamName": resources_core.FormatString(plan.TeamName), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_secrets", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_secrets", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "secrets", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "secrets", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "secrets", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *LlmAiSecretsResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state LlmAiSecretsModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *LlmAiSecretsResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan LlmAiSecretsModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state LlmAiSecretsModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.LlmAiID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{ + "keyName": resources_core.FormatString(plan.KeyName), + "models": resources_core.FormatString(plan.Models), + "rpmLimit": resources_core.FormatInt64(plan.RpmLimit), + "teamName": resources_core.FormatString(plan.TeamName), + "tpmLimit": resources_core.FormatInt64(plan.TpmLimit), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "secrets", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *LlmAiSecretsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state LlmAiSecretsModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_secrets" == "" { + return + } + + instanceUID := strings.TrimSpace(state.LlmAiID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "keyName": resources_core.FormatString(state.KeyName), + "models": resources_core.FormatString(state.Models), + "rpmLimit": resources_core.FormatInt64(state.RpmLimit), + "teamName": resources_core.FormatString(state.TeamName), + "tpmLimit": resources_core.FormatInt64(state.TpmLimit), + } + identityCodes := []string{ + "keyName", + "models", + "rpmLimit", + "teamName", + "tpmLimit", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("secrets", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "keyName": resources_core.FormatString(state.KeyName), + "teamName": resources_core.FormatString(state.TeamName), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_secrets", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *LlmAiSecretsResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/163_llm_ai_team_resource.go b/generated/test/go/163_llm_ai_team_resource.go new file mode 100644 index 0000000..e411362 --- /dev/null +++ b/generated/test/go/163_llm_ai_team_resource.go @@ -0,0 +1,365 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: llm_ai +// Subresource: team + +var _ resource.Resource = &LlmAiTeamResource{} + +// LlmAiTeamResource manages a subresource via operations. +type LlmAiTeamResource struct { + client *core.UniversalClient +} + +type LlmAiTeamModel struct { + ID types.String `tfsdk:"id"` + LlmAiID types.String `tfsdk:"llm_ai_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + Models types.String `tfsdk:"models"` + RpmLimit types.Int64 `tfsdk:"rpm_limit"` + TeamName types.String `tfsdk:"team_name"` + TpmLimit types.Int64 `tfsdk:"tpm_limit"` +} + +func NewLlmAiTeamResource() resource.Resource { + return &LlmAiTeamResource{} +} + +func (r *LlmAiTeamResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_llm_ai_team" +} + +func (r *LlmAiTeamResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "llm_ai_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "models": schema.StringAttribute{Required: true}, + "rpm_limit": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(60)}, + "team_name": schema.StringAttribute{Required: true, MarkdownDescription: "Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "tpm_limit": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100000)}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *LlmAiTeamResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan LlmAiTeamModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if plan.Models.IsNull() || plan.Models.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "models is required.") + return + } + if plan.TeamName.IsNull() || plan.TeamName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "team_name is required.") + return + } + + instanceUID := strings.TrimSpace(plan.LlmAiID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "teamName": resources_core.FormatString(plan.TeamName), + } + identityCodes := []string{ + "teamName", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("team", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "team", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "teamName": resources_core.FormatString(plan.TeamName), + "models": resources_core.FormatString(plan.Models), + "rpmLimit": resources_core.FormatInt64(plan.RpmLimit), + "tpmLimit": resources_core.FormatInt64(plan.TpmLimit), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_team", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_team", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "team", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "team", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "team", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *LlmAiTeamResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state LlmAiTeamModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *LlmAiTeamResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan LlmAiTeamModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "modify_team" == "" { + var state LlmAiTeamModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.LlmAiID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{ + "teamName": resources_core.FormatString(plan.TeamName), + "models": resources_core.FormatString(plan.Models), + "rpmLimit": resources_core.FormatInt64(plan.RpmLimit), + "tpmLimit": resources_core.FormatInt64(plan.TpmLimit), + }) + + idParams := map[string]string{ + "teamName": resources_core.FormatString(plan.TeamName), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "modify_team", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "team", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *LlmAiTeamResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state LlmAiTeamModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_team" == "" { + return + } + + instanceUID := strings.TrimSpace(state.LlmAiID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "teamName": resources_core.FormatString(state.TeamName), + } + identityCodes := []string{ + "teamName", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("team", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "teamName": resources_core.FormatString(state.TeamName), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_team", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *LlmAiTeamResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/19_vc_org_resource.go b/generated/test/go/19_vc_org_resource.go new file mode 100644 index 0000000..90d238a --- /dev/null +++ b/generated/test/go/19_vc_org_resource.go @@ -0,0 +1,391 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vc_org +// Service ID: 19 + +var _ resource.Resource = &VcOrgResource{} +var _ resource.ResourceWithModifyPlan = &VcOrgResource{} +var _ resource.ResourceWithImportState = &VcOrgResource{} + +type VcOrgResource struct { + client *core.UniversalClient +} + +type VcOrgModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + OrganizationType types.String `tfsdk:"organization_type"` + ResourceRealm types.String `tfsdk:"resource_realm"` + VIPConfigure types.String `tfsdk:"v_i_p_configure"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewVcOrgResource() resource.Resource { + return &VcOrgResource{} +} + +func (r *VcOrgResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vc_org" +} + +func (r *VcOrgResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "organization_type": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("iaas"), MarkdownDescription: "Доступ к организации предоставляется только при выборе `iaas` Не изменяется после создания."}, + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("sandbox.nubes.ru"), MarkdownDescription: "Облако в котором будет развёрнут тенант Не изменяется после создания."}, + "v_i_p_configure": schema.StringAttribute{Required: true}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VcOrgResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *VcOrgModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *VcOrgModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan VcOrgModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.OrganizationType.IsNull() && !plan.OrganizationType.IsUnknown() && !state.OrganizationType.IsNull() && !state.OrganizationType.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.OrganizationType.ValueString(), state.OrganizationType.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить organization_type", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.VIPConfigure.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "v_i_p_configure is required.") + return + } + if config.VIPConfigure.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 418: resources_core.FormatString(config.ResourceRealm), + 556: resources_core.FormatString(config.OrganizationType), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 19, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *VcOrgResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data VcOrgModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.VIPConfigure.IsNull() || data.VIPConfigure.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "v_i_p_configure is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 19, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 418: resources_core.FormatString(data.ResourceRealm), + 556: resources_core.FormatString(data.OrganizationType), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 19, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 19, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "organizationType", Field: "OrganizationType", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "vIPConfigure", Field: "VIPConfigure", Type: "array-map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcOrgResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VcOrgModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 19, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "organizationType", Field: "OrganizationType", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "vIPConfigure", Field: "VIPConfigure", Type: "array-map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *VcOrgResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VcOrgModel + var state VcOrgModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.VIPConfigure.IsNull() != state.VIPConfigure.IsNull() || plan.VIPConfigure.IsUnknown() != state.VIPConfigure.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.VIPConfigure.IsNull() && !plan.VIPConfigure.IsUnknown() && !state.VIPConfigure.IsNull() && !state.VIPConfigure.IsUnknown() { + if !strings.EqualFold(plan.VIPConfigure.ValueString(), state.VIPConfigure.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 662: resources_core.FormatString(plan.VIPConfigure), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 19, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "organizationType", Field: "OrganizationType", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "vIPConfigure", Field: "VIPConfigure", Type: "array-map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcOrgResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VcOrgModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *VcOrgResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *VcOrgResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/19_vc_org_user_resource.go b/generated/test/go/19_vc_org_user_resource.go new file mode 100644 index 0000000..b18fcdd --- /dev/null +++ b/generated/test/go/19_vc_org_user_resource.go @@ -0,0 +1,343 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vc_org +// Subresource: user + +var _ resource.Resource = &VcOrgUserResource{} + +// VcOrgUserResource manages a subresource via operations. +type VcOrgUserResource struct { + client *core.UniversalClient +} + +type VcOrgUserModel struct { + ID types.String `tfsdk:"id"` + VcOrgID types.String `tfsdk:"vc_org_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + Admins types.String `tfsdk:"admins"` +} + +func NewVcOrgUserResource() resource.Resource { + return &VcOrgUserResource{} +} + +func (r *VcOrgUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vc_org_user" +} + +func (r *VcOrgUserResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "vc_org_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "admins": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VcOrgUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan VcOrgUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.VcOrgID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "admins": resources_core.FormatString(plan.Admins), + } + identityCodes := []string{ + "admins", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "admins": resources_core.FormatString(plan.Admins), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *VcOrgUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VcOrgUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcOrgUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VcOrgUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state VcOrgUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.VcOrgID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{ + "admins": resources_core.FormatString(plan.Admins), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *VcOrgUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VcOrgUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_user" == "" { + return + } + + instanceUID := strings.TrimSpace(state.VcOrgID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "admins": resources_core.FormatString(state.Admins), + } + identityCodes := []string{ + "admins", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "admins": resources_core.FormatString(state.Admins), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_user", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *VcOrgUserResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/1_dummy_resource.go b/generated/test/go/1_dummy_resource.go new file mode 100644 index 0000000..a00d9cc --- /dev/null +++ b/generated/test/go/1_dummy_resource.go @@ -0,0 +1,582 @@ +package resources_gen + +import ( + "context" + "fmt" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: dummy +// Service ID: 1 + +var _ resource.Resource = &DummyResource{} +var _ resource.ResourceWithModifyPlan = &DummyResource{} +var _ resource.ResourceWithImportState = &DummyResource{} + +type DummyResource struct { + client *core.UniversalClient +} + +// DummyMapExampleModel — вложенная модель для map-fixed параметра mapExample. +type DummyMapExampleModel struct { + Subparam2 types.String `tfsdk:"subparam2" json:"subparam2"` + Subparam1 types.String `tfsdk:"subparam1" json:"subparam1"` + Secret types.String `tfsdk:"secret" json:"secret"` +} + +// DummyMapFixedModel — вложенная модель для map-fixed параметра mapFixed. +type DummyMapFixedModel struct { + Bol1 types.Bool `tfsdk:"bol1" json:"bol1"` + MinStr1 types.String `tfsdk:"min_str1" json:"minStr1"` + Param1 types.Int64 `tfsdk:"param1" json:"param1"` + Param2 types.String `tfsdk:"param2" json:"param2"` + Param3 types.Int64 `tfsdk:"param3" json:"param3"` +} + +type DummyModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + // --- Redeploy support (ARCHITECTURE.md) --- + // git_revision: при изменении вызывает redeploy вместо modify. + // Опциональное поле — если не задано, modify работает как обычно. + GitRevision types.String `tfsdk:"git_revision"` + Arr types.String `tfsdk:"arr"` + ArrayMapFixedExample types.String `tfsdk:"array_map_fixed_example"` + Bodymessage types.String `tfsdk:"bodymessage"` + DurationMs types.Int64 `tfsdk:"duration_ms"` + FailAtStart types.Bool `tfsdk:"fail_at_start"` + FailInProgress types.Bool `tfsdk:"fail_in_progress"` + JsonExample types.String `tfsdk:"json_example"` + MapExample *DummyMapExampleModel `tfsdk:"map_example"` + MapFixed *DummyMapFixedModel `tfsdk:"map_fixed"` + NestedRefExample types.String `tfsdk:"nested_ref_example"` + ResourceRealm types.String `tfsdk:"resource_realm"` + WhereFail types.Int64 `tfsdk:"where_fail"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewDummyResource() resource.Resource { + return &DummyResource{} +} + +func (r *DummyResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_dummy" +} + +func (r *DummyResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "arr": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания."}, + "array_map_fixed_example": schema.StringAttribute{Optional: true, MarkdownDescription: "Array of map-fixed Не изменяется после создания."}, + "bodymessage": schema.StringAttribute{Optional: true, MarkdownDescription: "Дата, уходящая в Vault"}, + "duration_ms": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(0), MarkdownDescription: "перед завершением подождать (sleep) столько миллисекунд"}, + "fail_at_start": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "если true, завершить джоб, ничего не записывая в cmdb"}, + "fail_in_progress": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "если true, корректно начать выполнение джоба, но в процессе завершить аварийно, ничего не записывая в cmdb"}, + "json_example": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "Для тестирования интерфейса", PlanModifiers: []planmodifier.String{resources_core.JsonNormalize()}}, + "map_example": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "subparam2": schema.StringAttribute{Optional: true}, + "subparam1": schema.StringAttribute{Optional: true, MarkdownDescription: "это для администратора"}, + "secret": schema.StringAttribute{Optional: true, MarkdownDescription: "это секрет"}, + }}, + "map_fixed": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "bol1": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Булевский, обязательный"}, + "min_str1": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("abracadabra"), MarkdownDescription: "не меньше 10 символов"}, + "param1": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Integer, secret"}, + "param2": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("1"), MarkdownDescription: "Дескрипшен"}, + "param3": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "целое положительное"}, + }}, + "nested_ref_example": schema.StringAttribute{Optional: true, MarkdownDescription: "Не изменяется после создания."}, + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("dummy"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + "where_fail": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "принимает 1,2,3. Работает с failInProgress. 1 - упасть в prepare. 2 - упасть внутри блока Data fill. 3 - упасть после vault write"}, + // --- Redeploy support (ARCHITECTURE.md) --- + // При изменении git_revision вызывается redeploy вместо modify. + // Если не задано — modify работает без изменений. + "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *DummyResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *DummyModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *DummyModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan DummyModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.Arr.IsNull() && !plan.Arr.IsUnknown() && !state.Arr.IsNull() && !state.Arr.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Arr.ValueString(), state.Arr.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить arr", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ArrayMapFixedExample.IsNull() && !plan.ArrayMapFixedExample.IsUnknown() && !state.ArrayMapFixedExample.IsNull() && !state.ArrayMapFixedExample.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ArrayMapFixedExample.ValueString(), state.ArrayMapFixedExample.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить array_map_fixed_example", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.NestedRefExample.IsNull() && !plan.NestedRefExample.IsUnknown() && !state.NestedRefExample.IsNull() && !state.NestedRefExample.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.NestedRefExample.ValueString(), state.NestedRefExample.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить nested_ref_example", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.Arr.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "arr is required.") + return + } + if config.Arr.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 198: resources_core.FormatInt64(config.DurationMs), + 199: resources_core.FormatBool(config.FailAtStart), + 200: resources_core.FormatBool(config.FailInProgress), + 201: resources_core.FormatInt64(config.WhereFail), + 242: resources_core.FormatString(config.ResourceRealm), + 286: resources_core.FormatString(config.Bodymessage), + 321: resources_core.BuildJSON(map[string]string{"subparam2": fmt.Sprintf("\"%s\"", config.MapExample.Subparam2.ValueString()), "subparam1": fmt.Sprintf("\"%s\"", config.MapExample.Subparam1.ValueString()), "secret": fmt.Sprintf("\"%s\"", config.MapExample.Secret.ValueString())}), + 322: resources_core.FormatString(config.JsonExample), + 396: resources_core.FormatString(config.NestedRefExample), + 647: resources_core.BuildJSON(map[string]string{"bol1": fmt.Sprintf("%v", config.MapFixed.Bol1.ValueBool()), "minStr1": fmt.Sprintf("\"%s\"", config.MapFixed.MinStr1.ValueString()), "param1": fmt.Sprintf("%d", config.MapFixed.Param1.ValueInt64()), "param2": fmt.Sprintf("\"%s\"", config.MapFixed.Param2.ValueString()), "param3": fmt.Sprintf("%d", config.MapFixed.Param3.ValueInt64())}), + 654: resources_core.FormatString(config.ArrayMapFixedExample), + 863: resources_core.FormatString(config.Arr), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 1, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *DummyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data DummyModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.Arr.IsNull() || data.Arr.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "arr is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 1, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 198: resources_core.FormatInt64(data.DurationMs), + 199: resources_core.FormatBool(data.FailAtStart), + 200: resources_core.FormatBool(data.FailInProgress), + 201: resources_core.FormatInt64(data.WhereFail), + 242: resources_core.FormatString(data.ResourceRealm), + 286: resources_core.FormatString(data.Bodymessage), + 321: resources_core.BuildJSON(map[string]string{"subparam2": fmt.Sprintf("\"%s\"", data.MapExample.Subparam2.ValueString()), "subparam1": fmt.Sprintf("\"%s\"", data.MapExample.Subparam1.ValueString()), "secret": fmt.Sprintf("\"%s\"", data.MapExample.Secret.ValueString())}), + 322: resources_core.FormatString(data.JsonExample), + 396: resources_core.FormatString(data.NestedRefExample), + 647: resources_core.BuildJSON(map[string]string{"bol1": fmt.Sprintf("%v", data.MapFixed.Bol1.ValueBool()), "minStr1": fmt.Sprintf("\"%s\"", data.MapFixed.MinStr1.ValueString()), "param1": fmt.Sprintf("%d", data.MapFixed.Param1.ValueInt64()), "param2": fmt.Sprintf("\"%s\"", data.MapFixed.Param2.ValueString()), "param3": fmt.Sprintf("%d", data.MapFixed.Param3.ValueInt64())}), + 654: resources_core.FormatString(data.ArrayMapFixedExample), + 863: resources_core.FormatString(data.Arr), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 1, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 1, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "arr", Field: "Arr", Type: "string"}, + {Code: "arrayMapFixedExample", Field: "ArrayMapFixedExample", Type: "array-map-fixed"}, + {Code: "bodymessage", Field: "Bodymessage", Type: "string"}, + {Code: "durationMs", Field: "DurationMs", Type: "int64"}, + {Code: "failAtStart", Field: "FailAtStart", Type: "bool"}, + {Code: "failInProgress", Field: "FailInProgress", Type: "bool"}, + {Code: "jsonExample", Field: "JsonExample", Type: "string"}, + {Code: "mapExample", Field: "MapExample", Type: "string"}, + {Code: "mapFixed", Field: "MapFixed", Type: "map-fixed"}, + {Code: "nestedRefExample", Field: "NestedRefExample", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "whereFail", Field: "WhereFail", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *DummyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state DummyModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 1, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "arr", Field: "Arr", Type: "string"}, + {Code: "arrayMapFixedExample", Field: "ArrayMapFixedExample", Type: "array-map-fixed"}, + {Code: "bodymessage", Field: "Bodymessage", Type: "string"}, + {Code: "durationMs", Field: "DurationMs", Type: "int64"}, + {Code: "failAtStart", Field: "FailAtStart", Type: "bool"}, + {Code: "failInProgress", Field: "FailInProgress", Type: "bool"}, + {Code: "jsonExample", Field: "JsonExample", Type: "string"}, + {Code: "mapExample", Field: "MapExample", Type: "string"}, + {Code: "mapFixed", Field: "MapFixed", Type: "map-fixed"}, + {Code: "nestedRefExample", Field: "NestedRefExample", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "whereFail", Field: "WhereFail", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *DummyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan DummyModel + var state DummyModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.DurationMs.IsNull() != state.DurationMs.IsNull() || plan.DurationMs.IsUnknown() != state.DurationMs.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.DurationMs.IsNull() && !plan.DurationMs.IsUnknown() && !state.DurationMs.IsNull() && !state.DurationMs.IsUnknown() { + if plan.DurationMs.ValueInt64() != state.DurationMs.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.FailAtStart.IsNull() != state.FailAtStart.IsNull() || plan.FailAtStart.IsUnknown() != state.FailAtStart.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.FailAtStart.IsNull() && !plan.FailAtStart.IsUnknown() && !state.FailAtStart.IsNull() && !state.FailAtStart.IsUnknown() { + if plan.FailAtStart.ValueBool() != state.FailAtStart.ValueBool() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.FailInProgress.IsNull() != state.FailInProgress.IsNull() || plan.FailInProgress.IsUnknown() != state.FailInProgress.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.FailInProgress.IsNull() && !plan.FailInProgress.IsUnknown() && !state.FailInProgress.IsNull() && !state.FailInProgress.IsUnknown() { + if plan.FailInProgress.ValueBool() != state.FailInProgress.ValueBool() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.WhereFail.IsNull() != state.WhereFail.IsNull() || plan.WhereFail.IsUnknown() != state.WhereFail.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.WhereFail.IsNull() && !plan.WhereFail.IsUnknown() && !state.WhereFail.IsNull() && !state.WhereFail.IsUnknown() { + if plan.WhereFail.ValueInt64() != state.WhereFail.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.Bodymessage.IsNull() != state.Bodymessage.IsNull() || plan.Bodymessage.IsUnknown() != state.Bodymessage.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.Bodymessage.IsNull() && !plan.Bodymessage.IsUnknown() && !state.Bodymessage.IsNull() && !state.Bodymessage.IsUnknown() { + if !strings.EqualFold(plan.Bodymessage.ValueString(), state.Bodymessage.ValueString()) { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.JsonExample.IsNull() != state.JsonExample.IsNull() || plan.JsonExample.IsUnknown() != state.JsonExample.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.JsonExample.IsNull() && !plan.JsonExample.IsUnknown() && !state.JsonExample.IsNull() && !state.JsonExample.IsUnknown() { + if !strings.EqualFold(plan.JsonExample.ValueString(), state.JsonExample.ValueString()) { + hasServiceParamChanges = true + } + } + } + // --- Redeploy support (ARCHITECTURE.md) --- + // Проверяем изменился ли git_revision. + // Если да — вызываем redeploy вместо modify. + // git_revision = "" (не задано) → обычный modify. + redeployRequested := false + if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() { + if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() { + redeployRequested = true + } + } + + if !hasServiceParamChanges && !redeployRequested { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + // --- Redeploy support (ARCHITECTURE.md) --- + if redeployRequested { + // Вызываем redeploy — пустой params (операция не требует CFS-параметров) + if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + } else { + params := map[int]string{ + 287: resources_core.FormatInt64(plan.DurationMs), + 288: resources_core.FormatBool(plan.FailAtStart), + 289: resources_core.FormatBool(plan.FailInProgress), + 290: resources_core.FormatInt64(plan.WhereFail), + 291: resources_core.FormatString(plan.Bodymessage), + 373: resources_core.BuildJSON(map[string]string{"secret": fmt.Sprintf("\"%s\"", plan.MapExample.Secret.ValueString())}), + 374: resources_core.FormatString(plan.JsonExample), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 1, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "arr", Field: "Arr", Type: "string"}, + {Code: "arrayMapFixedExample", Field: "ArrayMapFixedExample", Type: "array-map-fixed"}, + {Code: "bodymessage", Field: "Bodymessage", Type: "string"}, + {Code: "durationMs", Field: "DurationMs", Type: "int64"}, + {Code: "failAtStart", Field: "FailAtStart", Type: "bool"}, + {Code: "failInProgress", Field: "FailInProgress", Type: "bool"}, + {Code: "jsonExample", Field: "JsonExample", Type: "string"}, + {Code: "mapExample", Field: "MapExample", Type: "string"}, + {Code: "mapFixed", Field: "MapFixed", Type: "map-fixed"}, + {Code: "nestedRefExample", Field: "NestedRefExample", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "whereFail", Field: "WhereFail", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *DummyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state DummyModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *DummyResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *DummyResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/20_vc_org_saas_resource.go b/generated/test/go/20_vc_org_saas_resource.go new file mode 100644 index 0000000..c1277b1 --- /dev/null +++ b/generated/test/go/20_vc_org_saas_resource.go @@ -0,0 +1,348 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vc_org_saas +// Service ID: 20 + +var _ resource.Resource = &VcOrgSaasResource{} +var _ resource.ResourceWithModifyPlan = &VcOrgSaasResource{} +var _ resource.ResourceWithImportState = &VcOrgSaasResource{} + +type VcOrgSaasResource struct { + client *core.UniversalClient +} + +type VcOrgSaasModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + ResourceRealm types.String `tfsdk:"resource_realm"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewVcOrgSaasResource() resource.Resource { + return &VcOrgSaasResource{} +} + +func (r *VcOrgSaasResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vc_org_saas" +} + +func (r *VcOrgSaasResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("sandbox.nubes.ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VcOrgSaasResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *VcOrgSaasModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *VcOrgSaasModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan VcOrgSaasModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 185: resources_core.FormatString(config.ResourceRealm), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 20, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *VcOrgSaasResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data VcOrgSaasModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 20, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 185: resources_core.FormatString(data.ResourceRealm), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 20, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 20, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcOrgSaasResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VcOrgSaasModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 20, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *VcOrgSaasResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VcOrgSaasModel + var state VcOrgSaasModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 20, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcOrgSaasResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VcOrgSaasModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *VcOrgSaasResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *VcOrgSaasResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/21_vc_vdc_resource.go b/generated/test/go/21_vc_vdc_resource.go new file mode 100644 index 0000000..0c32692 --- /dev/null +++ b/generated/test/go/21_vc_vdc_resource.go @@ -0,0 +1,573 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vc_vdc +// Service ID: 21 + +var _ resource.Resource = &VcVdcResource{} +var _ resource.ResourceWithModifyPlan = &VcVdcResource{} +var _ resource.ResourceWithImportState = &VcVdcResource{} + +type VcVdcResource struct { + client *core.UniversalClient +} + +type VcVdcModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + CpuAllocated types.Int64 `tfsdk:"cpu_allocated"` + CpuGuaranteed types.Int64 `tfsdk:"cpu_guaranteed"` + MemAllocated types.Int64 `tfsdk:"mem_allocated"` + NetworkProvider types.String `tfsdk:"network_provider"` + OrganizationUid types.String `tfsdk:"organization_uid"` + ProviderVdc types.String `tfsdk:"provider_vdc"` + StorageConfig types.String `tfsdk:"storage_config"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewVcVdcResource() resource.Resource { + return &VcVdcResource{} +} + +func (r *VcVdcResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vc_vdc" +} + +func (r *VcVdcResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "cpu_allocated": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(80), MarkdownDescription: "шт"}, + "cpu_guaranteed": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(20), MarkdownDescription: "% резервирования Не изменяется после создания."}, + "mem_allocated": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(200), MarkdownDescription: "gb"}, + "network_provider": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания."}, + "organization_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "Для создания VDC предварительно необходимо создать организацию через услугу `Организация в Cloud Director` Не изменяется после создания."}, + "provider_vdc": schema.StringAttribute{Required: true, MarkdownDescription: "Приписка `2.8` - Скорость vCPU - 2800MHz
И так далее...
Не изменяется после создания."}, + "storage_config": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Первая политика в списке указывается как "По умолчанию""}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VcVdcResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *VcVdcModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *VcVdcModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan VcVdcModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan. + // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов. + // Resolve (displayName → UUID или uppercase → lowercase) нужен только для + // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка + // "Provider produced invalid plan: planned value does not match config value". + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.CpuGuaranteed.IsNull() && !plan.CpuGuaranteed.IsUnknown() && !state.CpuGuaranteed.IsNull() && !state.CpuGuaranteed.IsUnknown() { + if plan.CpuGuaranteed.ValueInt64() != state.CpuGuaranteed.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить cpu_guaranteed", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.NetworkProvider.IsNull() && !plan.NetworkProvider.IsUnknown() && !state.NetworkProvider.IsNull() && !state.NetworkProvider.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.NetworkProvider.ValueString(), state.NetworkProvider.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить network_provider", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.OrganizationUid.IsNull() && !plan.OrganizationUid.IsUnknown() && !state.OrganizationUid.IsNull() && !state.OrganizationUid.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.OrganizationUid.ValueString(), state.OrganizationUid.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить organization_uid", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ProviderVdc.IsNull() && !plan.ProviderVdc.IsUnknown() && !state.ProviderVdc.IsNull() && !state.ProviderVdc.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ProviderVdc.ValueString(), state.ProviderVdc.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить provider_vdc", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.NetworkProvider.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "network_provider is required.") + return + } + if config.NetworkProvider.IsUnknown() { + return + } + if config.OrganizationUid.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "organization_uid is required.") + return + } + if config.OrganizationUid.IsUnknown() { + return + } + if config.ProviderVdc.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "provider_vdc is required.") + return + } + if config.ProviderVdc.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + if !config.OrganizationUid.IsNull() && !config.OrganizationUid.IsUnknown() { + resolvedOrganizationUid, err := r.client.ResolveRefSvcParamValue(ctx, 19, config.OrganizationUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve organization_uid", err.Error()) + } else if resolvedOrganizationUid != "" && resolvedOrganizationUid != config.OrganizationUid.ValueString() { + config.OrganizationUid = types.StringValue(resolvedOrganizationUid) + } + } + params := map[int]string{ + 30: resources_core.FormatString(config.OrganizationUid), + 335: resources_core.FormatString(config.ProviderVdc), + 361: resources_core.FormatString(config.StorageConfig), + 397: resources_core.FormatInt64(config.CpuGuaranteed), + 557: resources_core.FormatInt64(config.CpuAllocated), + 558: resources_core.FormatInt64(config.MemAllocated), + 746: resources_core.FormatString(config.NetworkProvider), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 21, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *VcVdcResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data VcVdcModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.NetworkProvider.IsNull() || data.NetworkProvider.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "network_provider is required.") + return + } + if data.OrganizationUid.IsNull() || data.OrganizationUid.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "organization_uid is required.") + return + } + if data.ProviderVdc.IsNull() || data.ProviderVdc.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "provider_vdc is required.") + return + } + // ══════════════════════════════════════════════════════════════════════════ + // ПРАВИЛО TERRAFORM (официальная документация): + // «If an attribute value is configured, it is NEVER valid to change that + // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что + // написал пользователь). Менять plan запрещено на уровне фреймворка. + // + // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре. + // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный). + // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит: + // «Provider produced inconsistent result after apply». + // + // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот. + // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как + // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API). + // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный + // регистр в state через strings.EqualFold (сравниваем без учёта регистра). + // ══════════════════════════════════════════════════════════════════════════ + originalOrganizationUid := data.OrganizationUid + if !data.OrganizationUid.IsNull() && !data.OrganizationUid.IsUnknown() { + resolvedOrganizationUid, err := r.client.ResolveRefSvcParamValue(ctx, 19, data.OrganizationUid.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if resolvedOrganizationUid != "" && resolvedOrganizationUid != data.OrganizationUid.ValueString() { + data.OrganizationUid = types.StringValue(resolvedOrganizationUid) + } + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 21, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 30: resources_core.FormatString(data.OrganizationUid), + 335: resources_core.FormatString(data.ProviderVdc), + 361: resources_core.FormatString(data.StorageConfig), + 397: resources_core.FormatInt64(data.CpuGuaranteed), + 557: resources_core.FormatInt64(data.CpuAllocated), + 558: resources_core.FormatInt64(data.MemAllocated), + 746: resources_core.FormatString(data.NetworkProvider), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 21, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 21, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "cpuAllocated", Field: "CpuAllocated", Type: "int64"}, + {Code: "cpuGuaranteed", Field: "CpuGuaranteed", Type: "int64"}, + {Code: "memAllocated", Field: "MemAllocated", Type: "int64"}, + {Code: "networkProvider", Field: "NetworkProvider", Type: "string"}, + {Code: "organizationUid", Field: "OrganizationUid", Type: "string"}, + {Code: "providerVdc", Field: "ProviderVdc", Type: "string"}, + {Code: "storageConfig", Field: "StorageConfig", Type: "array-map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.OrganizationUid.IsNull() && !state.OrganizationUid.IsUnknown() { + resolvedOrganizationUid, err := r.client.ResolveRefSvcParamValue(ctx, 19, state.OrganizationUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve organization_uid", err.Error()) + } else if resolvedOrganizationUid != "" && resolvedOrganizationUid != state.OrganizationUid.ValueString() { + state.OrganizationUid = types.StringValue(resolvedOrganizationUid) + } + } + // Restore user-provided casing в state (Create). + // EqualFold = «совпадают ли значения без учёта регистра?» + // Если да — значит API вернул «туже» строку, только в другом регистре. + // Заменяем state на original (то что было в plan/config пользователя). + // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint. + if !originalOrganizationUid.IsNull() && !originalOrganizationUid.IsUnknown() && !state.OrganizationUid.IsNull() && !state.OrganizationUid.IsUnknown() { + if strings.EqualFold(state.OrganizationUid.ValueString(), originalOrganizationUid.ValueString()) { + state.OrganizationUid = originalOrganizationUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcVdcResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VcVdcModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 21, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "cpuAllocated", Field: "CpuAllocated", Type: "int64"}, + {Code: "cpuGuaranteed", Field: "CpuGuaranteed", Type: "int64"}, + {Code: "memAllocated", Field: "MemAllocated", Type: "int64"}, + {Code: "networkProvider", Field: "NetworkProvider", Type: "string"}, + {Code: "organizationUid", Field: "OrganizationUid", Type: "string"}, + {Code: "providerVdc", Field: "ProviderVdc", Type: "string"}, + {Code: "storageConfig", Field: "StorageConfig", Type: "array-map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !newState.OrganizationUid.IsNull() && !newState.OrganizationUid.IsUnknown() { + resolvedOrganizationUid, err := r.client.ResolveRefSvcParamValue(ctx, 19, newState.OrganizationUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve organization_uid", err.Error()) + } else if resolvedOrganizationUid != "" && resolvedOrganizationUid != newState.OrganizationUid.ValueString() { + newState.OrganizationUid = types.StringValue(resolvedOrganizationUid) + } + } + // Restore user-provided casing в state (Read). + // При чтении у нас нет plan — но предыдущий state уже хранит значение + // в регистре пользователя (после первого Create оно было восстановлено). + // Берём prior state (переменная state) как эталон регистра. + // Если API вернул то же UUID только строчными буквами — восстанавливаем. + if !state.OrganizationUid.IsNull() && !state.OrganizationUid.IsUnknown() && !newState.OrganizationUid.IsNull() && !newState.OrganizationUid.IsUnknown() { + if strings.EqualFold(newState.OrganizationUid.ValueString(), state.OrganizationUid.ValueString()) { + newState.OrganizationUid = state.OrganizationUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *VcVdcResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VcVdcModel + var state VcVdcModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.CpuAllocated.IsNull() != state.CpuAllocated.IsNull() || plan.CpuAllocated.IsUnknown() != state.CpuAllocated.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.CpuAllocated.IsNull() && !plan.CpuAllocated.IsUnknown() && !state.CpuAllocated.IsNull() && !state.CpuAllocated.IsUnknown() { + if plan.CpuAllocated.ValueInt64() != state.CpuAllocated.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.MemAllocated.IsNull() != state.MemAllocated.IsNull() || plan.MemAllocated.IsUnknown() != state.MemAllocated.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.MemAllocated.IsNull() && !plan.MemAllocated.IsUnknown() && !state.MemAllocated.IsNull() && !state.MemAllocated.IsUnknown() { + if plan.MemAllocated.ValueInt64() != state.MemAllocated.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.StorageConfig.IsNull() != state.StorageConfig.IsNull() || plan.StorageConfig.IsUnknown() != state.StorageConfig.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.StorageConfig.IsNull() && !plan.StorageConfig.IsUnknown() && !state.StorageConfig.IsNull() && !state.StorageConfig.IsUnknown() { + if !strings.EqualFold(plan.StorageConfig.ValueString(), state.StorageConfig.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 560: resources_core.FormatInt64(plan.CpuAllocated), + 561: resources_core.FormatInt64(plan.MemAllocated), + 562: resources_core.FormatString(plan.StorageConfig), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 21, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "cpuAllocated", Field: "CpuAllocated", Type: "int64"}, + {Code: "cpuGuaranteed", Field: "CpuGuaranteed", Type: "int64"}, + {Code: "memAllocated", Field: "MemAllocated", Type: "int64"}, + {Code: "networkProvider", Field: "NetworkProvider", Type: "string"}, + {Code: "organizationUid", Field: "OrganizationUid", Type: "string"}, + {Code: "providerVdc", Field: "ProviderVdc", Type: "string"}, + {Code: "storageConfig", Field: "StorageConfig", Type: "array-map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.OrganizationUid.IsNull() && !state.OrganizationUid.IsUnknown() { + resolvedOrganizationUid, err := r.client.ResolveRefSvcParamValue(ctx, 19, state.OrganizationUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve organization_uid", err.Error()) + } else if resolvedOrganizationUid != "" && resolvedOrganizationUid != state.OrganizationUid.ValueString() { + state.OrganizationUid = types.StringValue(resolvedOrganizationUid) + } + } + // Restore user-provided casing в state (Update). + // После API-вызова modify state содержит значения в lower-case от API. + // Plan == config == то что написал пользователь (регистр неизменён). + // EqualFold: если UUID совпадает без учёта регистра — берём из plan. + if !plan.OrganizationUid.IsNull() && !plan.OrganizationUid.IsUnknown() && !state.OrganizationUid.IsNull() && !state.OrganizationUid.IsUnknown() { + if strings.EqualFold(state.OrganizationUid.ValueString(), plan.OrganizationUid.ValueString()) { + state.OrganizationUid = plan.OrganizationUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcVdcResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VcVdcModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *VcVdcResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *VcVdcResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/22_vc_nsxt_resource.go b/generated/test/go/22_vc_nsxt_resource.go new file mode 100644 index 0000000..ca0acdb --- /dev/null +++ b/generated/test/go/22_vc_nsxt_resource.go @@ -0,0 +1,614 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vc_nsxt +// Service ID: 22 + +var _ resource.Resource = &VcNsxtResource{} +var _ resource.ResourceWithModifyPlan = &VcNsxtResource{} +var _ resource.ResourceWithImportState = &VcNsxtResource{} + +type VcNsxtResource struct { + client *core.UniversalClient +} + +type VcNsxtModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + IpSpaceName types.String `tfsdk:"ip_space_name"` + NeedEnableAVI types.Bool `tfsdk:"need_enable_a_v_i"` + QosProfile types.String `tfsdk:"qos_profile"` + VdcGroupUid types.String `tfsdk:"vdc_group_uid"` + VdcType types.String `tfsdk:"vdc_type"` + VdcUid types.String `tfsdk:"vdc_uid"` + VirtualServicesCount types.Int64 `tfsdk:"virtual_services_count"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewVcNsxtResource() resource.Resource { + return &VcNsxtResource{} +} + +func (r *VcNsxtResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vc_nsxt" +} + +func (r *VcNsxtResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "ip_space_name": schema.StringAttribute{Optional: true, MarkdownDescription: "Имя ip Space для внешнего IP"}, + "need_enable_a_v_i": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включение Load Balancer"}, + "qos_profile": schema.StringAttribute{Optional: true, MarkdownDescription: "Параметр пока не работает
Должен возвращать поле из ресурсной платформы типа vc из .vcd.hardware.gatewayQoSProfiles

Как по идее должен работать.
Нужно заполнить или CFS vdcUid, или vdcGroupUid
Идеально конечно проверять `if (params.vdcType == "vdc" && params.vdcUid != "")` или `if (params.vdcType == "vdcGroup" && params.vdcGroupUid != "")`

Далее надо пойти по стейту vdc -> org -> resPlatform, взять gatewayQoSProfiles
Или пойти по стейту vdcgroup -> vdc -> org -> resPlatform, взять gatewayQoSProfiles

Регулярку могу написать (Виталя)"}, + "vdc_group_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UUID Услуги `Группа виртуальных датацентров (groupvDC)` Не изменяется после создания."}, + "vdc_type": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("vdc"), MarkdownDescription: "Тип родительской услуги Не изменяется после создания."}, + "vdc_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UUID Услуги `Виртуальный датацентр (vDC)` Не изменяется после создания."}, + "virtual_services_count": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Кол-во виртуальных сервисов, которые **резервируются** на AVI"}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VcNsxtResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *VcNsxtModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *VcNsxtModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan VcNsxtModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan. + // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов. + // Resolve (displayName → UUID или uppercase → lowercase) нужен только для + // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка + // "Provider produced invalid plan: planned value does not match config value". + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.VdcGroupUid.IsNull() && !plan.VdcGroupUid.IsUnknown() && !state.VdcGroupUid.IsNull() && !state.VdcGroupUid.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VdcGroupUid.ValueString(), state.VdcGroupUid.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить vdc_group_uid", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.VdcType.IsNull() && !plan.VdcType.IsUnknown() && !state.VdcType.IsNull() && !state.VdcType.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VdcType.ValueString(), state.VdcType.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить vdc_type", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.VdcUid.IsNull() && !plan.VdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VdcUid.ValueString(), state.VdcUid.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить vdc_uid", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + if !config.VdcGroupUid.IsNull() && !config.VdcGroupUid.IsUnknown() { + resolvedVdcGroupUid, err := r.client.ResolveRefSvcParamValue(ctx, 29, config.VdcGroupUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_group_uid", err.Error()) + } else if resolvedVdcGroupUid != "" && resolvedVdcGroupUid != config.VdcGroupUid.ValueString() { + config.VdcGroupUid = types.StringValue(resolvedVdcGroupUid) + } + } + if !config.VdcUid.IsNull() && !config.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, config.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error()) + } else if resolvedVdcUid != "" && resolvedVdcUid != config.VdcUid.ValueString() { + config.VdcUid = types.StringValue(resolvedVdcUid) + } + } + params := map[int]string{ + 8: resources_core.FormatString(config.VdcUid), + 340: resources_core.FormatBool(config.NeedEnableAVI), + 341: resources_core.FormatInt64(config.VirtualServicesCount), + 621: resources_core.FormatString(config.VdcType), + 622: resources_core.FormatString(config.VdcGroupUid), + 825: resources_core.FormatString(config.QosProfile), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 22, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *VcNsxtResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data VcNsxtModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + // ══════════════════════════════════════════════════════════════════════════ + // ПРАВИЛО TERRAFORM (официальная документация): + // «If an attribute value is configured, it is NEVER valid to change that + // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что + // написал пользователь). Менять plan запрещено на уровне фреймворка. + // + // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре. + // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный). + // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит: + // «Provider produced inconsistent result after apply». + // + // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот. + // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как + // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API). + // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный + // регистр в state через strings.EqualFold (сравниваем без учёта регистра). + // ══════════════════════════════════════════════════════════════════════════ + originalVdcGroupUid := data.VdcGroupUid + // ══════════════════════════════════════════════════════════════════════════ + // ПРАВИЛО TERRAFORM (официальная документация): + // «If an attribute value is configured, it is NEVER valid to change that + // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что + // написал пользователь). Менять plan запрещено на уровне фреймворка. + // + // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре. + // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный). + // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит: + // «Provider produced inconsistent result after apply». + // + // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот. + // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как + // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API). + // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный + // регистр в state через strings.EqualFold (сравниваем без учёта регистра). + // ══════════════════════════════════════════════════════════════════════════ + originalVdcUid := data.VdcUid + if !data.VdcGroupUid.IsNull() && !data.VdcGroupUid.IsUnknown() { + resolvedVdcGroupUid, err := r.client.ResolveRefSvcParamValue(ctx, 29, data.VdcGroupUid.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if resolvedVdcGroupUid != "" && resolvedVdcGroupUid != data.VdcGroupUid.ValueString() { + data.VdcGroupUid = types.StringValue(resolvedVdcGroupUid) + } + } + if !data.VdcUid.IsNull() && !data.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, data.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if resolvedVdcUid != "" && resolvedVdcUid != data.VdcUid.ValueString() { + data.VdcUid = types.StringValue(resolvedVdcUid) + } + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 22, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 8: resources_core.FormatString(data.VdcUid), + 340: resources_core.FormatBool(data.NeedEnableAVI), + 341: resources_core.FormatInt64(data.VirtualServicesCount), + 621: resources_core.FormatString(data.VdcType), + 622: resources_core.FormatString(data.VdcGroupUid), + 825: resources_core.FormatString(data.QosProfile), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 22, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 22, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "needEnableAVI", Field: "NeedEnableAVI", Type: "bool"}, + {Code: "qosProfile", Field: "QosProfile", Type: "string"}, + {Code: "vdcGroupUid", Field: "VdcGroupUid", Type: "string"}, + {Code: "vdcType", Field: "VdcType", Type: "string"}, + {Code: "vdcUid", Field: "VdcUid", Type: "string"}, + {Code: "virtualServicesCount", Field: "VirtualServicesCount", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.VdcGroupUid.IsNull() && !state.VdcGroupUid.IsUnknown() { + resolvedVdcGroupUid, err := r.client.ResolveRefSvcParamValue(ctx, 29, state.VdcGroupUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_group_uid", err.Error()) + } else if resolvedVdcGroupUid != "" && resolvedVdcGroupUid != state.VdcGroupUid.ValueString() { + state.VdcGroupUid = types.StringValue(resolvedVdcGroupUid) + } + } + if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, state.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error()) + } else if resolvedVdcUid != "" && resolvedVdcUid != state.VdcUid.ValueString() { + state.VdcUid = types.StringValue(resolvedVdcUid) + } + } + // Restore user-provided casing в state (Create). + // EqualFold = «совпадают ли значения без учёта регистра?» + // Если да — значит API вернул «туже» строку, только в другом регистре. + // Заменяем state на original (то что было в plan/config пользователя). + // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint. + if !originalVdcGroupUid.IsNull() && !originalVdcGroupUid.IsUnknown() && !state.VdcGroupUid.IsNull() && !state.VdcGroupUid.IsUnknown() { + if strings.EqualFold(state.VdcGroupUid.ValueString(), originalVdcGroupUid.ValueString()) { + state.VdcGroupUid = originalVdcGroupUid + } + } + if !originalVdcUid.IsNull() && !originalVdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + if strings.EqualFold(state.VdcUid.ValueString(), originalVdcUid.ValueString()) { + state.VdcUid = originalVdcUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcNsxtResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VcNsxtModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 22, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "needEnableAVI", Field: "NeedEnableAVI", Type: "bool"}, + {Code: "qosProfile", Field: "QosProfile", Type: "string"}, + {Code: "vdcGroupUid", Field: "VdcGroupUid", Type: "string"}, + {Code: "vdcType", Field: "VdcType", Type: "string"}, + {Code: "vdcUid", Field: "VdcUid", Type: "string"}, + {Code: "virtualServicesCount", Field: "VirtualServicesCount", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !newState.VdcGroupUid.IsNull() && !newState.VdcGroupUid.IsUnknown() { + resolvedVdcGroupUid, err := r.client.ResolveRefSvcParamValue(ctx, 29, newState.VdcGroupUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_group_uid", err.Error()) + } else if resolvedVdcGroupUid != "" && resolvedVdcGroupUid != newState.VdcGroupUid.ValueString() { + newState.VdcGroupUid = types.StringValue(resolvedVdcGroupUid) + } + } + if !newState.VdcUid.IsNull() && !newState.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, newState.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error()) + } else if resolvedVdcUid != "" && resolvedVdcUid != newState.VdcUid.ValueString() { + newState.VdcUid = types.StringValue(resolvedVdcUid) + } + } + // Restore user-provided casing в state (Read). + // При чтении у нас нет plan — но предыдущий state уже хранит значение + // в регистре пользователя (после первого Create оно было восстановлено). + // Берём prior state (переменная state) как эталон регистра. + // Если API вернул то же UUID только строчными буквами — восстанавливаем. + if !state.VdcGroupUid.IsNull() && !state.VdcGroupUid.IsUnknown() && !newState.VdcGroupUid.IsNull() && !newState.VdcGroupUid.IsUnknown() { + if strings.EqualFold(newState.VdcGroupUid.ValueString(), state.VdcGroupUid.ValueString()) { + newState.VdcGroupUid = state.VdcGroupUid + } + } + if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() && !newState.VdcUid.IsNull() && !newState.VdcUid.IsUnknown() { + if strings.EqualFold(newState.VdcUid.ValueString(), state.VdcUid.ValueString()) { + newState.VdcUid = state.VdcUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *VcNsxtResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VcNsxtModel + var state VcNsxtModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.NeedEnableAVI.IsNull() != state.NeedEnableAVI.IsNull() || plan.NeedEnableAVI.IsUnknown() != state.NeedEnableAVI.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.NeedEnableAVI.IsNull() && !plan.NeedEnableAVI.IsUnknown() && !state.NeedEnableAVI.IsNull() && !state.NeedEnableAVI.IsUnknown() { + if plan.NeedEnableAVI.ValueBool() != state.NeedEnableAVI.ValueBool() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.VirtualServicesCount.IsNull() != state.VirtualServicesCount.IsNull() || plan.VirtualServicesCount.IsUnknown() != state.VirtualServicesCount.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.VirtualServicesCount.IsNull() && !plan.VirtualServicesCount.IsUnknown() && !state.VirtualServicesCount.IsNull() && !state.VirtualServicesCount.IsUnknown() { + if plan.VirtualServicesCount.ValueInt64() != state.VirtualServicesCount.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.IpSpaceName.IsNull() != state.IpSpaceName.IsNull() || plan.IpSpaceName.IsUnknown() != state.IpSpaceName.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.IpSpaceName.IsNull() && !plan.IpSpaceName.IsUnknown() && !state.IpSpaceName.IsNull() && !state.IpSpaceName.IsUnknown() { + if !strings.EqualFold(plan.IpSpaceName.ValueString(), state.IpSpaceName.ValueString()) { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.QosProfile.IsNull() != state.QosProfile.IsNull() || plan.QosProfile.IsUnknown() != state.QosProfile.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.QosProfile.IsNull() && !plan.QosProfile.IsUnknown() && !state.QosProfile.IsNull() && !state.QosProfile.IsUnknown() { + if !strings.EqualFold(plan.QosProfile.ValueString(), state.QosProfile.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 368: resources_core.FormatBool(plan.NeedEnableAVI), + 369: resources_core.FormatInt64(plan.VirtualServicesCount), + 372: resources_core.FormatString(plan.IpSpaceName), + 856: resources_core.FormatString(plan.QosProfile), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 22, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "needEnableAVI", Field: "NeedEnableAVI", Type: "bool"}, + {Code: "qosProfile", Field: "QosProfile", Type: "string"}, + {Code: "vdcGroupUid", Field: "VdcGroupUid", Type: "string"}, + {Code: "vdcType", Field: "VdcType", Type: "string"}, + {Code: "vdcUid", Field: "VdcUid", Type: "string"}, + {Code: "virtualServicesCount", Field: "VirtualServicesCount", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.VdcGroupUid.IsNull() && !state.VdcGroupUid.IsUnknown() { + resolvedVdcGroupUid, err := r.client.ResolveRefSvcParamValue(ctx, 29, state.VdcGroupUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_group_uid", err.Error()) + } else if resolvedVdcGroupUid != "" && resolvedVdcGroupUid != state.VdcGroupUid.ValueString() { + state.VdcGroupUid = types.StringValue(resolvedVdcGroupUid) + } + } + if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, state.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error()) + } else if resolvedVdcUid != "" && resolvedVdcUid != state.VdcUid.ValueString() { + state.VdcUid = types.StringValue(resolvedVdcUid) + } + } + // Restore user-provided casing в state (Update). + // После API-вызова modify state содержит значения в lower-case от API. + // Plan == config == то что написал пользователь (регистр неизменён). + // EqualFold: если UUID совпадает без учёта регистра — берём из plan. + if !plan.VdcGroupUid.IsNull() && !plan.VdcGroupUid.IsUnknown() && !state.VdcGroupUid.IsNull() && !state.VdcGroupUid.IsUnknown() { + if strings.EqualFold(state.VdcGroupUid.ValueString(), plan.VdcGroupUid.ValueString()) { + state.VdcGroupUid = plan.VdcGroupUid + } + } + if !plan.VdcUid.IsNull() && !plan.VdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + if strings.EqualFold(state.VdcUid.ValueString(), plan.VdcUid.ValueString()) { + state.VdcUid = plan.VdcUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcNsxtResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VcNsxtModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *VcNsxtResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *VcNsxtResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/23_vc_vm_resource.go b/generated/test/go/23_vc_vm_resource.go new file mode 100644 index 0000000..67aea2c --- /dev/null +++ b/generated/test/go/23_vc_vm_resource.go @@ -0,0 +1,581 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vc_vm +// Service ID: 23 + +var _ resource.Resource = &VcVmResource{} +var _ resource.ResourceWithModifyPlan = &VcVmResource{} +var _ resource.ResourceWithImportState = &VcVmResource{} + +type VcVmResource struct { + client *core.UniversalClient +} + +type VcVmModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessIpList types.String `tfsdk:"access_ip_list"` + IpSpaceName types.String `tfsdk:"ip_space_name"` + TemplateName types.String `tfsdk:"template_name"` + VappUid types.String `tfsdk:"vapp_uid"` + VmCpu types.String `tfsdk:"vm_cpu"` + VmDisksize types.String `tfsdk:"vm_disksize"` + VmName types.String `tfsdk:"vm_name"` + VmRam types.String `tfsdk:"vm_ram"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewVcVmResource() resource.Resource { + return &VcVmResource{} +} + +func (r *VcVmResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vc_vm" +} + +func (r *VcVmResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_ip_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("79.111.119.78/32 185.247.187.1/32"), MarkdownDescription: "Вставить содержимое из 'curl ifconfig.io'
IP адреса в CIDR формате через пробел для настройки аксес листов"}, + "ip_space_name": schema.StringAttribute{Required: true, MarkdownDescription: "Брать из свойств ресурсной платформы
.vcd.ipSpace

Имя IP-space для выделения внешнего IP (IP выделяется обязательно) Не изменяется после создания."}, + "template_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("RockyLinux_9-16G"), MarkdownDescription: "ОС Не изменяется после создания."}, + "vapp_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "InstanceUid vApp. vApp должен быть создан Не изменяется после создания."}, + "vm_cpu": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("2"), MarkdownDescription: "CPU"}, + "vm_disksize": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("10"), MarkdownDescription: "Размер диска под сервис (GB)"}, + "vm_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("vm0001"), MarkdownDescription: "Имя ВМ. Не поддерживает спец-символы Не изменяется после создания."}, + "vm_ram": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("2"), MarkdownDescription: "RAM (GB)"}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VcVmResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *VcVmModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *VcVmModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan VcVmModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan. + // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов. + // Resolve (displayName → UUID или uppercase → lowercase) нужен только для + // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка + // "Provider produced invalid plan: planned value does not match config value". + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.IpSpaceName.IsNull() && !plan.IpSpaceName.IsUnknown() && !state.IpSpaceName.IsNull() && !state.IpSpaceName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.IpSpaceName.ValueString(), state.IpSpaceName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить ip_space_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.TemplateName.IsNull() && !plan.TemplateName.IsUnknown() && !state.TemplateName.IsNull() && !state.TemplateName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.TemplateName.ValueString(), state.TemplateName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить template_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.VappUid.IsNull() && !plan.VappUid.IsUnknown() && !state.VappUid.IsNull() && !state.VappUid.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VappUid.ValueString(), state.VappUid.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить vapp_uid", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.VmName.IsNull() && !plan.VmName.IsUnknown() && !state.VmName.IsNull() && !state.VmName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VmName.ValueString(), state.VmName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить vm_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.IpSpaceName.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "ip_space_name is required.") + return + } + if config.IpSpaceName.IsUnknown() { + return + } + if config.VappUid.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "vapp_uid is required.") + return + } + if config.VappUid.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + if !config.VappUid.IsNull() && !config.VappUid.IsUnknown() { + resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, config.VappUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error()) + } else if resolvedVappUid != "" && resolvedVappUid != config.VappUid.ValueString() { + config.VappUid = types.StringValue(resolvedVappUid) + } + } + params := map[int]string{ + 33: resources_core.FormatString(config.VappUid), + 35: resources_core.FormatString(config.VmName), + 36: resources_core.FormatString(config.VmCpu), + 37: resources_core.FormatString(config.VmRam), + 38: resources_core.FormatString(config.VmDisksize), + 176: resources_core.FormatString(config.TemplateName), + 177: resources_core.FormatString(config.AccessIpList), + 406: resources_core.FormatString(config.IpSpaceName), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 23, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *VcVmResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data VcVmModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.IpSpaceName.IsNull() || data.IpSpaceName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "ip_space_name is required.") + return + } + if data.VappUid.IsNull() || data.VappUid.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "vapp_uid is required.") + return + } + // ══════════════════════════════════════════════════════════════════════════ + // ПРАВИЛО TERRAFORM (официальная документация): + // «If an attribute value is configured, it is NEVER valid to change that + // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что + // написал пользователь). Менять plan запрещено на уровне фреймворка. + // + // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре. + // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный). + // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит: + // «Provider produced inconsistent result after apply». + // + // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот. + // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как + // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API). + // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный + // регистр в state через strings.EqualFold (сравниваем без учёта регистра). + // ══════════════════════════════════════════════════════════════════════════ + originalVappUid := data.VappUid + if !data.VappUid.IsNull() && !data.VappUid.IsUnknown() { + resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, data.VappUid.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if resolvedVappUid != "" && resolvedVappUid != data.VappUid.ValueString() { + data.VappUid = types.StringValue(resolvedVappUid) + } + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 23, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 33: resources_core.FormatString(data.VappUid), + 35: resources_core.FormatString(data.VmName), + 36: resources_core.FormatString(data.VmCpu), + 37: resources_core.FormatString(data.VmRam), + 38: resources_core.FormatString(data.VmDisksize), + 176: resources_core.FormatString(data.TemplateName), + 177: resources_core.FormatString(data.AccessIpList), + 406: resources_core.FormatString(data.IpSpaceName), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 23, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 23, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessIpList", Field: "AccessIpList", Type: "string"}, + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "templateName", Field: "TemplateName", Type: "string"}, + {Code: "vappUid", Field: "VappUid", Type: "string"}, + {Code: "vmCpu", Field: "VmCpu", Type: "string"}, + {Code: "vmDisksize", Field: "VmDisksize", Type: "string"}, + {Code: "vmName", Field: "VmName", Type: "string"}, + {Code: "vmRam", Field: "VmRam", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.VappUid.IsNull() && !state.VappUid.IsUnknown() { + resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, state.VappUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error()) + } else if resolvedVappUid != "" && resolvedVappUid != state.VappUid.ValueString() { + state.VappUid = types.StringValue(resolvedVappUid) + } + } + // Restore user-provided casing в state (Create). + // EqualFold = «совпадают ли значения без учёта регистра?» + // Если да — значит API вернул «туже» строку, только в другом регистре. + // Заменяем state на original (то что было в plan/config пользователя). + // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint. + if !originalVappUid.IsNull() && !originalVappUid.IsUnknown() && !state.VappUid.IsNull() && !state.VappUid.IsUnknown() { + if strings.EqualFold(state.VappUid.ValueString(), originalVappUid.ValueString()) { + state.VappUid = originalVappUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcVmResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VcVmModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 23, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessIpList", Field: "AccessIpList", Type: "string"}, + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "templateName", Field: "TemplateName", Type: "string"}, + {Code: "vappUid", Field: "VappUid", Type: "string"}, + {Code: "vmCpu", Field: "VmCpu", Type: "string"}, + {Code: "vmDisksize", Field: "VmDisksize", Type: "string"}, + {Code: "vmName", Field: "VmName", Type: "string"}, + {Code: "vmRam", Field: "VmRam", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !newState.VappUid.IsNull() && !newState.VappUid.IsUnknown() { + resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, newState.VappUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error()) + } else if resolvedVappUid != "" && resolvedVappUid != newState.VappUid.ValueString() { + newState.VappUid = types.StringValue(resolvedVappUid) + } + } + // Restore user-provided casing в state (Read). + // При чтении у нас нет plan — но предыдущий state уже хранит значение + // в регистре пользователя (после первого Create оно было восстановлено). + // Берём prior state (переменная state) как эталон регистра. + // Если API вернул то же UUID только строчными буквами — восстанавливаем. + if !state.VappUid.IsNull() && !state.VappUid.IsUnknown() && !newState.VappUid.IsNull() && !newState.VappUid.IsUnknown() { + if strings.EqualFold(newState.VappUid.ValueString(), state.VappUid.ValueString()) { + newState.VappUid = state.VappUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *VcVmResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VcVmModel + var state VcVmModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.VmCpu.IsNull() != state.VmCpu.IsNull() || plan.VmCpu.IsUnknown() != state.VmCpu.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.VmCpu.IsNull() && !plan.VmCpu.IsUnknown() && !state.VmCpu.IsNull() && !state.VmCpu.IsUnknown() { + if !strings.EqualFold(plan.VmCpu.ValueString(), state.VmCpu.ValueString()) { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.VmRam.IsNull() != state.VmRam.IsNull() || plan.VmRam.IsUnknown() != state.VmRam.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.VmRam.IsNull() && !plan.VmRam.IsUnknown() && !state.VmRam.IsNull() && !state.VmRam.IsUnknown() { + if !strings.EqualFold(plan.VmRam.ValueString(), state.VmRam.ValueString()) { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.VmDisksize.IsNull() != state.VmDisksize.IsNull() || plan.VmDisksize.IsUnknown() != state.VmDisksize.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.VmDisksize.IsNull() && !plan.VmDisksize.IsUnknown() && !state.VmDisksize.IsNull() && !state.VmDisksize.IsUnknown() { + if !strings.EqualFold(plan.VmDisksize.ValueString(), state.VmDisksize.ValueString()) { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.AccessIpList.IsNull() != state.AccessIpList.IsNull() || plan.AccessIpList.IsUnknown() != state.AccessIpList.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.AccessIpList.IsNull() && !plan.AccessIpList.IsUnknown() && !state.AccessIpList.IsNull() && !state.AccessIpList.IsUnknown() { + if !strings.EqualFold(plan.AccessIpList.ValueString(), state.AccessIpList.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 186: resources_core.FormatString(plan.VmCpu), + 187: resources_core.FormatString(plan.VmRam), + 188: resources_core.FormatString(plan.VmDisksize), + 189: resources_core.FormatString(plan.AccessIpList), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 23, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessIpList", Field: "AccessIpList", Type: "string"}, + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "templateName", Field: "TemplateName", Type: "string"}, + {Code: "vappUid", Field: "VappUid", Type: "string"}, + {Code: "vmCpu", Field: "VmCpu", Type: "string"}, + {Code: "vmDisksize", Field: "VmDisksize", Type: "string"}, + {Code: "vmName", Field: "VmName", Type: "string"}, + {Code: "vmRam", Field: "VmRam", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.VappUid.IsNull() && !state.VappUid.IsUnknown() { + resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, state.VappUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error()) + } else if resolvedVappUid != "" && resolvedVappUid != state.VappUid.ValueString() { + state.VappUid = types.StringValue(resolvedVappUid) + } + } + // Restore user-provided casing в state (Update). + // После API-вызова modify state содержит значения в lower-case от API. + // Plan == config == то что написал пользователь (регистр неизменён). + // EqualFold: если UUID совпадает без учёта регистра — берём из plan. + if !plan.VappUid.IsNull() && !plan.VappUid.IsUnknown() && !state.VappUid.IsNull() && !state.VappUid.IsUnknown() { + if strings.EqualFold(state.VappUid.ValueString(), plan.VappUid.ValueString()) { + state.VappUid = plan.VappUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcVmResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VcVmModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *VcVmResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *VcVmResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/25_vcexternalip_resource.go b/generated/test/go/25_vcexternalip_resource.go new file mode 100644 index 0000000..686e188 --- /dev/null +++ b/generated/test/go/25_vcexternalip_resource.go @@ -0,0 +1,604 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vcexternalip +// Service ID: 25 + +var _ resource.Resource = &VcexternalipResource{} +var _ resource.ResourceWithModifyPlan = &VcexternalipResource{} +var _ resource.ResourceWithImportState = &VcexternalipResource{} + +type VcexternalipResource struct { + client *core.UniversalClient +} + +type VcexternalipModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + DnatCreate types.Bool `tfsdk:"dnat_create"` + FromServiceCloudEdgeName types.String `tfsdk:"from_service_cloud_edge_name"` + FromServiceCloudEdgeScope types.String `tfsdk:"from_service_cloud_edge_scope"` + FromServiceCloudOrgName types.String `tfsdk:"from_service_cloud_org_name"` + FromServiceCloudVdcName types.String `tfsdk:"from_service_cloud_vdc_name"` + FromServiceCloudVmwareUrl types.String `tfsdk:"from_service_cloud_vmware_url"` + FromServiceNamespace types.String `tfsdk:"from_service_namespace"` + FromServiceVdcGroupName types.String `tfsdk:"from_service_vdc_group_name"` + InternalAddrAccess types.String `tfsdk:"internal_addr_access"` + InternalPortAccess types.String `tfsdk:"internal_port_access"` + IpSpaceName types.String `tfsdk:"ip_space_name"` + ResourceRealm types.String `tfsdk:"resource_realm"` + ServiceUid types.String `tfsdk:"service_uid"` + SnatCreate types.Bool `tfsdk:"snat_create"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewVcexternalipResource() resource.Resource { + return &VcexternalipResource{} +} + +func (r *VcexternalipResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vcexternalip" +} + +func (r *VcexternalipResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "dnat_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Необходимо создать DNAT-правило."}, + "from_service_cloud_edge_name": schema.StringAttribute{Optional: true, MarkdownDescription: "Edge составной услуги (указывать, если resourceRealm связан с CloudDirector) Не изменяется после создания."}, + "from_service_cloud_edge_scope": schema.StringAttribute{Optional: true, MarkdownDescription: "Не изменяется после создания."}, + "from_service_cloud_org_name": schema.StringAttribute{Optional: true, MarkdownDescription: "Org составной услуги (указывать, если resourceRealm связан с CloudDirector) Не изменяется после создания."}, + "from_service_cloud_vdc_name": schema.StringAttribute{Optional: true, MarkdownDescription: "Vdc составной услуги (указывать, если resourceRealm связан с CloudDirector) Не изменяется после создания."}, + "from_service_cloud_vmware_url": schema.StringAttribute{Optional: true, MarkdownDescription: "Api-Url составной услуги (указывать, если resourceRealm связан с CloudDirector) Не изменяется после создания."}, + "from_service_namespace": schema.StringAttribute{Required: true, MarkdownDescription: "Namespace составной услуги (для отображения правил FW) Не изменяется после создания."}, + "from_service_vdc_group_name": schema.StringAttribute{Optional: true, MarkdownDescription: "Не изменяется после создания."}, + "internal_addr_access": schema.StringAttribute{Required: true, MarkdownDescription: "Внутренний адрес для связки ната. Требует dnatCreate=true или snatCreate=true"}, + "internal_port_access": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("5432"), MarkdownDescription: "Порт для связки ната с адресом. Несколько значений можно передавать через пробел. Если передать all, то dnat будет на все порты. Требует dnatCreate=true
"}, + "ip_space_name": schema.StringAttribute{Required: true, MarkdownDescription: "Тип ip Space (internet-ipv4-v1 / internet-antiddos-v1 / internet-no-antiddos-v1)"}, + "resource_realm": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("sandbox.nubes.ru"), MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + "service_uid": schema.StringAttribute{Required: true}, + "snat_create": schema.BoolAttribute{Optional: true, MarkdownDescription: "Необходимо создать SNAT-правило.
"}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VcexternalipResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *VcexternalipModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *VcexternalipModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan VcexternalipModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.FromServiceCloudEdgeName.IsNull() && !plan.FromServiceCloudEdgeName.IsUnknown() && !state.FromServiceCloudEdgeName.IsNull() && !state.FromServiceCloudEdgeName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.FromServiceCloudEdgeName.ValueString(), state.FromServiceCloudEdgeName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить from_service_cloud_edge_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.FromServiceCloudEdgeScope.IsNull() && !plan.FromServiceCloudEdgeScope.IsUnknown() && !state.FromServiceCloudEdgeScope.IsNull() && !state.FromServiceCloudEdgeScope.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.FromServiceCloudEdgeScope.ValueString(), state.FromServiceCloudEdgeScope.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить from_service_cloud_edge_scope", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.FromServiceCloudOrgName.IsNull() && !plan.FromServiceCloudOrgName.IsUnknown() && !state.FromServiceCloudOrgName.IsNull() && !state.FromServiceCloudOrgName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.FromServiceCloudOrgName.ValueString(), state.FromServiceCloudOrgName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить from_service_cloud_org_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.FromServiceCloudVdcName.IsNull() && !plan.FromServiceCloudVdcName.IsUnknown() && !state.FromServiceCloudVdcName.IsNull() && !state.FromServiceCloudVdcName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.FromServiceCloudVdcName.ValueString(), state.FromServiceCloudVdcName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить from_service_cloud_vdc_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.FromServiceCloudVmwareUrl.IsNull() && !plan.FromServiceCloudVmwareUrl.IsUnknown() && !state.FromServiceCloudVmwareUrl.IsNull() && !state.FromServiceCloudVmwareUrl.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.FromServiceCloudVmwareUrl.ValueString(), state.FromServiceCloudVmwareUrl.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить from_service_cloud_vmware_url", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.FromServiceNamespace.IsNull() && !plan.FromServiceNamespace.IsUnknown() && !state.FromServiceNamespace.IsNull() && !state.FromServiceNamespace.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.FromServiceNamespace.ValueString(), state.FromServiceNamespace.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить from_service_namespace", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.FromServiceVdcGroupName.IsNull() && !plan.FromServiceVdcGroupName.IsUnknown() && !state.FromServiceVdcGroupName.IsNull() && !state.FromServiceVdcGroupName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.FromServiceVdcGroupName.ValueString(), state.FromServiceVdcGroupName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить from_service_vdc_group_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.FromServiceNamespace.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "from_service_namespace is required.") + return + } + if config.FromServiceNamespace.IsUnknown() { + return + } + if config.InternalAddrAccess.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "internal_addr_access is required.") + return + } + if config.InternalAddrAccess.IsUnknown() { + return + } + if config.IpSpaceName.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "ip_space_name is required.") + return + } + if config.IpSpaceName.IsUnknown() { + return + } + if config.ServiceUid.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "service_uid is required.") + return + } + if config.ServiceUid.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 140: resources_core.FormatString(config.ServiceUid), + 141: resources_core.FormatBool(config.DnatCreate), + 142: resources_core.FormatString(config.InternalPortAccess), + 143: resources_core.FormatBool(config.SnatCreate), + 144: resources_core.FormatString(config.InternalAddrAccess), + 324: resources_core.FormatString(config.FromServiceNamespace), + 325: resources_core.FormatString(config.FromServiceCloudEdgeName), + 326: resources_core.FormatString(config.FromServiceCloudVdcName), + 327: resources_core.FormatString(config.FromServiceCloudOrgName), + 328: resources_core.FormatString(config.FromServiceCloudVmwareUrl), + 336: resources_core.FormatString(config.IpSpaceName), + 488: resources_core.FormatString(config.ResourceRealm), + 624: resources_core.FormatString(config.FromServiceCloudEdgeScope), + 626: resources_core.FormatString(config.FromServiceVdcGroupName), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 25, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *VcexternalipResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data VcexternalipModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.FromServiceNamespace.IsNull() || data.FromServiceNamespace.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "from_service_namespace is required.") + return + } + if data.InternalAddrAccess.IsNull() || data.InternalAddrAccess.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "internal_addr_access is required.") + return + } + if data.IpSpaceName.IsNull() || data.IpSpaceName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "ip_space_name is required.") + return + } + if data.ServiceUid.IsNull() || data.ServiceUid.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "service_uid is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 25, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 140: resources_core.FormatString(data.ServiceUid), + 141: resources_core.FormatBool(data.DnatCreate), + 142: resources_core.FormatString(data.InternalPortAccess), + 143: resources_core.FormatBool(data.SnatCreate), + 144: resources_core.FormatString(data.InternalAddrAccess), + 324: resources_core.FormatString(data.FromServiceNamespace), + 325: resources_core.FormatString(data.FromServiceCloudEdgeName), + 326: resources_core.FormatString(data.FromServiceCloudVdcName), + 327: resources_core.FormatString(data.FromServiceCloudOrgName), + 328: resources_core.FormatString(data.FromServiceCloudVmwareUrl), + 336: resources_core.FormatString(data.IpSpaceName), + 488: resources_core.FormatString(data.ResourceRealm), + 624: resources_core.FormatString(data.FromServiceCloudEdgeScope), + 626: resources_core.FormatString(data.FromServiceVdcGroupName), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 25, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 25, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "dnatCreate", Field: "DnatCreate", Type: "bool"}, + {Code: "fromServiceCloudEdgeName", Field: "FromServiceCloudEdgeName", Type: "string"}, + {Code: "fromServiceCloudEdgeScope", Field: "FromServiceCloudEdgeScope", Type: "string"}, + {Code: "fromServiceCloudOrgName", Field: "FromServiceCloudOrgName", Type: "string"}, + {Code: "fromServiceCloudVdcName", Field: "FromServiceCloudVdcName", Type: "string"}, + {Code: "fromServiceCloudVmwareUrl", Field: "FromServiceCloudVmwareUrl", Type: "string"}, + {Code: "fromServiceNamespace", Field: "FromServiceNamespace", Type: "string"}, + {Code: "fromServiceVdcGroupName", Field: "FromServiceVdcGroupName", Type: "string"}, + {Code: "internalAddrAccess", Field: "InternalAddrAccess", Type: "string"}, + {Code: "internalPortAccess", Field: "InternalPortAccess", Type: "string"}, + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "serviceUid", Field: "ServiceUid", Type: "string"}, + {Code: "snatCreate", Field: "SnatCreate", Type: "bool"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcexternalipResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VcexternalipModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 25, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "dnatCreate", Field: "DnatCreate", Type: "bool"}, + {Code: "fromServiceCloudEdgeName", Field: "FromServiceCloudEdgeName", Type: "string"}, + {Code: "fromServiceCloudEdgeScope", Field: "FromServiceCloudEdgeScope", Type: "string"}, + {Code: "fromServiceCloudOrgName", Field: "FromServiceCloudOrgName", Type: "string"}, + {Code: "fromServiceCloudVdcName", Field: "FromServiceCloudVdcName", Type: "string"}, + {Code: "fromServiceCloudVmwareUrl", Field: "FromServiceCloudVmwareUrl", Type: "string"}, + {Code: "fromServiceNamespace", Field: "FromServiceNamespace", Type: "string"}, + {Code: "fromServiceVdcGroupName", Field: "FromServiceVdcGroupName", Type: "string"}, + {Code: "internalAddrAccess", Field: "InternalAddrAccess", Type: "string"}, + {Code: "internalPortAccess", Field: "InternalPortAccess", Type: "string"}, + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "serviceUid", Field: "ServiceUid", Type: "string"}, + {Code: "snatCreate", Field: "SnatCreate", Type: "bool"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *VcexternalipResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VcexternalipModel + var state VcexternalipModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.ServiceUid.IsNull() != state.ServiceUid.IsNull() || plan.ServiceUid.IsUnknown() != state.ServiceUid.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ServiceUid.IsNull() && !plan.ServiceUid.IsUnknown() && !state.ServiceUid.IsNull() && !state.ServiceUid.IsUnknown() { + if !strings.EqualFold(plan.ServiceUid.ValueString(), state.ServiceUid.ValueString()) { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.IpSpaceName.IsNull() != state.IpSpaceName.IsNull() || plan.IpSpaceName.IsUnknown() != state.IpSpaceName.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.IpSpaceName.IsNull() && !plan.IpSpaceName.IsUnknown() && !state.IpSpaceName.IsNull() && !state.IpSpaceName.IsUnknown() { + if !strings.EqualFold(plan.IpSpaceName.ValueString(), state.IpSpaceName.ValueString()) { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.DnatCreate.IsNull() != state.DnatCreate.IsNull() || plan.DnatCreate.IsUnknown() != state.DnatCreate.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.DnatCreate.IsNull() && !plan.DnatCreate.IsUnknown() && !state.DnatCreate.IsNull() && !state.DnatCreate.IsUnknown() { + if plan.DnatCreate.ValueBool() != state.DnatCreate.ValueBool() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.SnatCreate.IsNull() != state.SnatCreate.IsNull() || plan.SnatCreate.IsUnknown() != state.SnatCreate.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.SnatCreate.IsNull() && !plan.SnatCreate.IsUnknown() && !state.SnatCreate.IsNull() && !state.SnatCreate.IsUnknown() { + if plan.SnatCreate.ValueBool() != state.SnatCreate.ValueBool() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.InternalPortAccess.IsNull() != state.InternalPortAccess.IsNull() || plan.InternalPortAccess.IsUnknown() != state.InternalPortAccess.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.InternalPortAccess.IsNull() && !plan.InternalPortAccess.IsUnknown() && !state.InternalPortAccess.IsNull() && !state.InternalPortAccess.IsUnknown() { + if !strings.EqualFold(plan.InternalPortAccess.ValueString(), state.InternalPortAccess.ValueString()) { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.InternalAddrAccess.IsNull() != state.InternalAddrAccess.IsNull() || plan.InternalAddrAccess.IsUnknown() != state.InternalAddrAccess.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.InternalAddrAccess.IsNull() && !plan.InternalAddrAccess.IsUnknown() && !state.InternalAddrAccess.IsNull() && !state.InternalAddrAccess.IsUnknown() { + if !strings.EqualFold(plan.InternalAddrAccess.ValueString(), state.InternalAddrAccess.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 627: resources_core.FormatString(plan.ServiceUid), + 628: resources_core.FormatString(plan.IpSpaceName), + 629: resources_core.FormatBool(plan.DnatCreate), + 630: resources_core.FormatBool(plan.SnatCreate), + 631: resources_core.FormatString(plan.InternalPortAccess), + 632: resources_core.FormatString(plan.InternalAddrAccess), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 25, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "dnatCreate", Field: "DnatCreate", Type: "bool"}, + {Code: "fromServiceCloudEdgeName", Field: "FromServiceCloudEdgeName", Type: "string"}, + {Code: "fromServiceCloudEdgeScope", Field: "FromServiceCloudEdgeScope", Type: "string"}, + {Code: "fromServiceCloudOrgName", Field: "FromServiceCloudOrgName", Type: "string"}, + {Code: "fromServiceCloudVdcName", Field: "FromServiceCloudVdcName", Type: "string"}, + {Code: "fromServiceCloudVmwareUrl", Field: "FromServiceCloudVmwareUrl", Type: "string"}, + {Code: "fromServiceNamespace", Field: "FromServiceNamespace", Type: "string"}, + {Code: "fromServiceVdcGroupName", Field: "FromServiceVdcGroupName", Type: "string"}, + {Code: "internalAddrAccess", Field: "InternalAddrAccess", Type: "string"}, + {Code: "internalPortAccess", Field: "InternalPortAccess", Type: "string"}, + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + {Code: "serviceUid", Field: "ServiceUid", Type: "string"}, + {Code: "snatCreate", Field: "SnatCreate", Type: "bool"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcexternalipResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VcexternalipModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *VcexternalipResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *VcexternalipResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/26_vapp_resource.go b/generated/test/go/26_vapp_resource.go new file mode 100644 index 0000000..68d1ac6 --- /dev/null +++ b/generated/test/go/26_vapp_resource.go @@ -0,0 +1,571 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vapp +// Service ID: 26 + +var _ resource.Resource = &VappResource{} +var _ resource.ResourceWithModifyPlan = &VappResource{} +var _ resource.ResourceWithImportState = &VappResource{} + +type VappResource struct { + client *core.UniversalClient +} + +type VappModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + NsxtUid types.String `tfsdk:"nsxt_uid"` + VappName types.String `tfsdk:"vapp_name"` + VdcUid types.String `tfsdk:"vdc_uid"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewVappResource() resource.Resource { + return &VappResource{} +} + +func (r *VappResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vapp" +} + +func (r *VappResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "nsxt_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UUID Услуги `Сетевой шлюз периметра (Edge)` Не изменяется после создания."}, + "vapp_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("lvifmmxj"), MarkdownDescription: "Уникальное в рамках организации Не изменяется после создания."}, + "vdc_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UUID Услуги `Виртуальный датацентр (vDC)` Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VappResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *VappModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *VappModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan VappModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan. + // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов. + // Resolve (displayName → UUID или uppercase → lowercase) нужен только для + // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка + // "Provider produced invalid plan: planned value does not match config value". + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.NsxtUid.IsNull() && !plan.NsxtUid.IsUnknown() && !state.NsxtUid.IsNull() && !state.NsxtUid.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.NsxtUid.ValueString(), state.NsxtUid.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить nsxt_uid", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.VappName.IsNull() && !plan.VappName.IsUnknown() && !state.VappName.IsNull() && !state.VappName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VappName.ValueString(), state.VappName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить vapp_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.VdcUid.IsNull() && !plan.VdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VdcUid.ValueString(), state.VdcUid.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить vdc_uid", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.NsxtUid.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "nsxt_uid is required.") + return + } + if config.NsxtUid.IsUnknown() { + return + } + if config.VdcUid.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "vdc_uid is required.") + return + } + if config.VdcUid.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + if !config.NsxtUid.IsNull() && !config.NsxtUid.IsUnknown() { + resolvedNsxtUid, err := r.client.ResolveRefSvcParamValue(ctx, 22, config.NsxtUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve nsxt_uid", err.Error()) + } else if resolvedNsxtUid != "" && resolvedNsxtUid != config.NsxtUid.ValueString() { + config.NsxtUid = types.StringValue(resolvedNsxtUid) + } + } + if !config.VdcUid.IsNull() && !config.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, config.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error()) + } else if resolvedVdcUid != "" && resolvedVdcUid != config.VdcUid.ValueString() { + config.VdcUid = types.StringValue(resolvedVdcUid) + } + } + params := map[int]string{ + 190: resources_core.FormatString(config.NsxtUid), + 191: resources_core.FormatString(config.VappName), + 623: resources_core.FormatString(config.VdcUid), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 26, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *VappResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data VappModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.NsxtUid.IsNull() || data.NsxtUid.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "nsxt_uid is required.") + return + } + if data.VdcUid.IsNull() || data.VdcUid.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "vdc_uid is required.") + return + } + // ══════════════════════════════════════════════════════════════════════════ + // ПРАВИЛО TERRAFORM (официальная документация): + // «If an attribute value is configured, it is NEVER valid to change that + // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что + // написал пользователь). Менять plan запрещено на уровне фреймворка. + // + // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре. + // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный). + // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит: + // «Provider produced inconsistent result after apply». + // + // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот. + // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как + // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API). + // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный + // регистр в state через strings.EqualFold (сравниваем без учёта регистра). + // ══════════════════════════════════════════════════════════════════════════ + originalNsxtUid := data.NsxtUid + // ══════════════════════════════════════════════════════════════════════════ + // ПРАВИЛО TERRAFORM (официальная документация): + // «If an attribute value is configured, it is NEVER valid to change that + // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что + // написал пользователь). Менять plan запрещено на уровне фреймворка. + // + // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре. + // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный). + // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит: + // «Provider produced inconsistent result after apply». + // + // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот. + // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как + // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API). + // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный + // регистр в state через strings.EqualFold (сравниваем без учёта регистра). + // ══════════════════════════════════════════════════════════════════════════ + originalVdcUid := data.VdcUid + if !data.NsxtUid.IsNull() && !data.NsxtUid.IsUnknown() { + resolvedNsxtUid, err := r.client.ResolveRefSvcParamValue(ctx, 22, data.NsxtUid.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if resolvedNsxtUid != "" && resolvedNsxtUid != data.NsxtUid.ValueString() { + data.NsxtUid = types.StringValue(resolvedNsxtUid) + } + } + if !data.VdcUid.IsNull() && !data.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, data.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if resolvedVdcUid != "" && resolvedVdcUid != data.VdcUid.ValueString() { + data.VdcUid = types.StringValue(resolvedVdcUid) + } + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 26, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 190: resources_core.FormatString(data.NsxtUid), + 191: resources_core.FormatString(data.VappName), + 623: resources_core.FormatString(data.VdcUid), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 26, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 26, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "nsxtUid", Field: "NsxtUid", Type: "string"}, + {Code: "vappName", Field: "VappName", Type: "string"}, + {Code: "vdcUid", Field: "VdcUid", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.NsxtUid.IsNull() && !state.NsxtUid.IsUnknown() { + resolvedNsxtUid, err := r.client.ResolveRefSvcParamValue(ctx, 22, state.NsxtUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve nsxt_uid", err.Error()) + } else if resolvedNsxtUid != "" && resolvedNsxtUid != state.NsxtUid.ValueString() { + state.NsxtUid = types.StringValue(resolvedNsxtUid) + } + } + if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, state.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error()) + } else if resolvedVdcUid != "" && resolvedVdcUid != state.VdcUid.ValueString() { + state.VdcUid = types.StringValue(resolvedVdcUid) + } + } + // Restore user-provided casing в state (Create). + // EqualFold = «совпадают ли значения без учёта регистра?» + // Если да — значит API вернул «туже» строку, только в другом регистре. + // Заменяем state на original (то что было в plan/config пользователя). + // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint. + if !originalNsxtUid.IsNull() && !originalNsxtUid.IsUnknown() && !state.NsxtUid.IsNull() && !state.NsxtUid.IsUnknown() { + if strings.EqualFold(state.NsxtUid.ValueString(), originalNsxtUid.ValueString()) { + state.NsxtUid = originalNsxtUid + } + } + if !originalVdcUid.IsNull() && !originalVdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + if strings.EqualFold(state.VdcUid.ValueString(), originalVdcUid.ValueString()) { + state.VdcUid = originalVdcUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VappResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VappModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 26, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "nsxtUid", Field: "NsxtUid", Type: "string"}, + {Code: "vappName", Field: "VappName", Type: "string"}, + {Code: "vdcUid", Field: "VdcUid", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !newState.NsxtUid.IsNull() && !newState.NsxtUid.IsUnknown() { + resolvedNsxtUid, err := r.client.ResolveRefSvcParamValue(ctx, 22, newState.NsxtUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve nsxt_uid", err.Error()) + } else if resolvedNsxtUid != "" && resolvedNsxtUid != newState.NsxtUid.ValueString() { + newState.NsxtUid = types.StringValue(resolvedNsxtUid) + } + } + if !newState.VdcUid.IsNull() && !newState.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, newState.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error()) + } else if resolvedVdcUid != "" && resolvedVdcUid != newState.VdcUid.ValueString() { + newState.VdcUid = types.StringValue(resolvedVdcUid) + } + } + // Restore user-provided casing в state (Read). + // При чтении у нас нет plan — но предыдущий state уже хранит значение + // в регистре пользователя (после первого Create оно было восстановлено). + // Берём prior state (переменная state) как эталон регистра. + // Если API вернул то же UUID только строчными буквами — восстанавливаем. + if !state.NsxtUid.IsNull() && !state.NsxtUid.IsUnknown() && !newState.NsxtUid.IsNull() && !newState.NsxtUid.IsUnknown() { + if strings.EqualFold(newState.NsxtUid.ValueString(), state.NsxtUid.ValueString()) { + newState.NsxtUid = state.NsxtUid + } + } + if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() && !newState.VdcUid.IsNull() && !newState.VdcUid.IsUnknown() { + if strings.EqualFold(newState.VdcUid.ValueString(), state.VdcUid.ValueString()) { + newState.VdcUid = state.VdcUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *VappResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VappModel + var state VappModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 26, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "nsxtUid", Field: "NsxtUid", Type: "string"}, + {Code: "vappName", Field: "VappName", Type: "string"}, + {Code: "vdcUid", Field: "VdcUid", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.NsxtUid.IsNull() && !state.NsxtUid.IsUnknown() { + resolvedNsxtUid, err := r.client.ResolveRefSvcParamValue(ctx, 22, state.NsxtUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve nsxt_uid", err.Error()) + } else if resolvedNsxtUid != "" && resolvedNsxtUid != state.NsxtUid.ValueString() { + state.NsxtUid = types.StringValue(resolvedNsxtUid) + } + } + if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, state.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error()) + } else if resolvedVdcUid != "" && resolvedVdcUid != state.VdcUid.ValueString() { + state.VdcUid = types.StringValue(resolvedVdcUid) + } + } + // Restore user-provided casing в state (Update). + // После API-вызова modify state содержит значения в lower-case от API. + // Plan == config == то что написал пользователь (регистр неизменён). + // EqualFold: если UUID совпадает без учёта регистра — берём из plan. + if !plan.NsxtUid.IsNull() && !plan.NsxtUid.IsUnknown() && !state.NsxtUid.IsNull() && !state.NsxtUid.IsUnknown() { + if strings.EqualFold(state.NsxtUid.ValueString(), plan.NsxtUid.ValueString()) { + state.NsxtUid = plan.NsxtUid + } + } + if !plan.VdcUid.IsNull() && !plan.VdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + if strings.EqualFold(state.VdcUid.ValueString(), plan.VdcUid.ValueString()) { + state.VdcUid = plan.VdcUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VappResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VappModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *VappResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *VappResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/27_vc_vm_v2_resource.go b/generated/test/go/27_vc_vm_v2_resource.go new file mode 100644 index 0000000..0fe1861 --- /dev/null +++ b/generated/test/go/27_vc_vm_v2_resource.go @@ -0,0 +1,350 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vc_vm_v2 +// Service ID: 27 + +var _ resource.Resource = &VcVmV2Resource{} +var _ resource.ResourceWithModifyPlan = &VcVmV2Resource{} +var _ resource.ResourceWithImportState = &VcVmV2Resource{} + +type VcVmV2Resource struct { + client *core.UniversalClient +} + +type VcVmV2Model struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + ServerListJson types.String `tfsdk:"server_list_json"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewVcVmV2Resource() resource.Resource { + return &VcVmV2Resource{} +} + +func (r *VcVmV2Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vc_vm_v2" +} + +func (r *VcVmV2Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "server_list_json": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[{}]"), MarkdownDescription: "Необходимо изменить vappUid и ipSpaceName
Пример конфигурации взять по ссылке ниже

Объект серверов вида [{}] (Map(Object))
Более подробнее смотреть в README.md (https://gitea.mgmt.nubes.ru/cloudServices/vcVmV2)", PlanModifiers: []planmodifier.String{resources_core.JsonNormalize()}}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VcVmV2Resource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *VcVmV2Model + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *VcVmV2Model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan VcVmV2Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 301: resources_core.FormatString(config.ServerListJson), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 27, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *VcVmV2Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data VcVmV2Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 27, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 301: resources_core.FormatString(data.ServerListJson), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 27, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 27, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "serverListJson", Field: "ServerListJson", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcVmV2Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VcVmV2Model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 27, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "serverListJson", Field: "ServerListJson", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *VcVmV2Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VcVmV2Model + var state VcVmV2Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.ServerListJson.IsNull() != state.ServerListJson.IsNull() || plan.ServerListJson.IsUnknown() != state.ServerListJson.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ServerListJson.IsNull() && !plan.ServerListJson.IsUnknown() && !state.ServerListJson.IsNull() && !state.ServerListJson.IsUnknown() { + if !strings.EqualFold(plan.ServerListJson.ValueString(), state.ServerListJson.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 302: resources_core.FormatString(plan.ServerListJson), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 27, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "serverListJson", Field: "ServerListJson", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcVmV2Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VcVmV2Model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *VcVmV2Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *VcVmV2Resource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/28_vc_vm_v3_resource.go b/generated/test/go/28_vc_vm_v3_resource.go new file mode 100644 index 0000000..58d967f --- /dev/null +++ b/generated/test/go/28_vc_vm_v3_resource.go @@ -0,0 +1,697 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vc_vm_v3 +// Service ID: 28 + +var _ resource.Resource = &VcVmV3Resource{} +var _ resource.ResourceWithModifyPlan = &VcVmV3Resource{} +var _ resource.ResourceWithImportState = &VcVmV3Resource{} + +type VcVmV3Resource struct { + client *core.UniversalClient +} + +type VcVmV3Model struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + // --- Redeploy support (ARCHITECTURE.md) --- + // git_revision: при изменении вызывает redeploy вместо modify. + // Опциональное поле — если не задано, modify работает как обычно. + GitRevision types.String `tfsdk:"git_revision"` + AccessIpList types.String `tfsdk:"access_ip_list"` + AccessPortList types.String `tfsdk:"access_port_list"` + CloudInit types.String `tfsdk:"cloud_init"` + ImageVm types.String `tfsdk:"image_vm"` + IpSpaceName types.String `tfsdk:"ip_space_name"` + UserLogin types.String `tfsdk:"user_login"` + UserPublicKey types.String `tfsdk:"user_public_key"` + VappUid types.String `tfsdk:"vapp_uid"` + VmCpu types.Int64 `tfsdk:"vm_cpu"` + VmDisk types.Int64 `tfsdk:"vm_disk"` + VmName types.String `tfsdk:"vm_name"` + VmRam types.Int64 `tfsdk:"vm_ram"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewVcVmV3Resource() resource.Resource { + return &VcVmV3Resource{} +} + +func (r *VcVmV3Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vc_vm_v3" +} + +func (r *VcVmV3Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_ip_list": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "Формат массива
Каким адресам будет предоставлен доступ до вм

Требует выделенного внешнего IP
Если ничего не передано, то выставлено ["0.0.0.0/0"]", PlanModifiers: []planmodifier.String{resources_core.JsonNormalize()}}, + "access_port_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[\"22\"]"), MarkdownDescription: "По каким портам открыть доступ к ВМ.
Порты 22, 10050 проставляются автоматически
Пока доступно:
22 - SSH,
80 - HTTP,
443 - HTTPS,
10050 - ZABBIX"}, + "cloud_init": schema.StringAttribute{Optional: true, MarkdownDescription: "Можно передать создание дополнительных пользователей.
На текущий момент не реализовано выполнения в cloud-init, установки пакетов, выполнения команд и записи файлов. Не изменяется после создания."}, + "image_vm": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания."}, + "ip_space_name": schema.StringAttribute{Required: true}, + "user_login": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("myuser"), MarkdownDescription: "Учётка SSH Не изменяется после создания."}, + "user_public_key": schema.StringAttribute{Required: true, MarkdownDescription: "Публичная часть SSH-Ключа для пользователя Не изменяется после создания."}, + "vapp_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UI display_name for service_id=26 (mapped to UUID in core) Не изменяется после создания."}, + "vm_cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "шт"}, + "vm_disk": schema.Int64Attribute{Optional: true, MarkdownDescription: "Основной диск у vm зависит от образа (см. документацию)
Если необходимо дополнительное место для vm, введите колличество Gb в эту форму"}, + "vm_name": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("web01"), MarkdownDescription: "Не изменяется после создания."}, + "vm_ram": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Gb"}, + // --- Redeploy support (ARCHITECTURE.md) --- + // При изменении git_revision вызывается redeploy вместо modify. + // Если не задано — modify работает без изменений. + "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VcVmV3Resource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *VcVmV3Model + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *VcVmV3Model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan VcVmV3Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan. + // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов. + // Resolve (displayName → UUID или uppercase → lowercase) нужен только для + // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка + // "Provider produced invalid plan: planned value does not match config value". + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.CloudInit.IsNull() && !plan.CloudInit.IsUnknown() && !state.CloudInit.IsNull() && !state.CloudInit.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.CloudInit.ValueString(), state.CloudInit.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить cloud_init", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ImageVm.IsNull() && !plan.ImageVm.IsUnknown() && !state.ImageVm.IsNull() && !state.ImageVm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ImageVm.ValueString(), state.ImageVm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить image_vm", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.UserLogin.IsNull() && !plan.UserLogin.IsUnknown() && !state.UserLogin.IsNull() && !state.UserLogin.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.UserLogin.ValueString(), state.UserLogin.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить user_login", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.UserPublicKey.IsNull() && !plan.UserPublicKey.IsUnknown() && !state.UserPublicKey.IsNull() && !state.UserPublicKey.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.UserPublicKey.ValueString(), state.UserPublicKey.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить user_public_key", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.VappUid.IsNull() && !plan.VappUid.IsUnknown() && !state.VappUid.IsNull() && !state.VappUid.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VappUid.ValueString(), state.VappUid.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить vapp_uid", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.VmName.IsNull() && !plan.VmName.IsUnknown() && !state.VmName.IsNull() && !state.VmName.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VmName.ValueString(), state.VmName.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить vm_name", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.ImageVm.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "image_vm is required.") + return + } + if config.ImageVm.IsUnknown() { + return + } + if config.IpSpaceName.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "ip_space_name is required.") + return + } + if config.IpSpaceName.IsUnknown() { + return + } + if config.UserPublicKey.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "user_public_key is required.") + return + } + if config.UserPublicKey.IsUnknown() { + return + } + if config.VappUid.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "vapp_uid is required.") + return + } + if config.VappUid.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + if !config.VappUid.IsNull() && !config.VappUid.IsUnknown() { + resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, config.VappUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error()) + } else if resolvedVappUid != "" && resolvedVappUid != config.VappUid.ValueString() { + config.VappUid = types.StringValue(resolvedVappUid) + } + } + params := map[int]string{ + 407: resources_core.FormatString(config.VappUid), + 408: resources_core.FormatString(config.VmName), + 409: resources_core.FormatInt64(config.VmCpu), + 410: resources_core.FormatInt64(config.VmRam), + 411: resources_core.FormatInt64(config.VmDisk), + 412: resources_core.FormatString(config.IpSpaceName), + 413: resources_core.FormatString(config.AccessIpList), + 414: resources_core.FormatString(config.ImageVm), + 415: resources_core.FormatString(config.CloudInit), + 416: resources_core.FormatString(config.UserLogin), + 417: resources_core.FormatString(config.UserPublicKey), + 448: resources_core.FormatString(config.AccessPortList), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 28, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *VcVmV3Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data VcVmV3Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.ImageVm.IsNull() || data.ImageVm.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "image_vm is required.") + return + } + if data.IpSpaceName.IsNull() || data.IpSpaceName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "ip_space_name is required.") + return + } + if data.UserPublicKey.IsNull() || data.UserPublicKey.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "user_public_key is required.") + return + } + if data.VappUid.IsNull() || data.VappUid.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "vapp_uid is required.") + return + } + // ══════════════════════════════════════════════════════════════════════════ + // ПРАВИЛО TERRAFORM (официальная документация): + // «If an attribute value is configured, it is NEVER valid to change that + // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что + // написал пользователь). Менять plan запрещено на уровне фреймворка. + // + // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре. + // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный). + // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит: + // «Provider produced inconsistent result after apply». + // + // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот. + // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как + // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API). + // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный + // регистр в state через strings.EqualFold (сравниваем без учёта регистра). + // ══════════════════════════════════════════════════════════════════════════ + originalVappUid := data.VappUid + if !data.VappUid.IsNull() && !data.VappUid.IsUnknown() { + resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, data.VappUid.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if resolvedVappUid != "" && resolvedVappUid != data.VappUid.ValueString() { + data.VappUid = types.StringValue(resolvedVappUid) + } + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 28, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 407: resources_core.FormatString(data.VappUid), + 408: resources_core.FormatString(data.VmName), + 409: resources_core.FormatInt64(data.VmCpu), + 410: resources_core.FormatInt64(data.VmRam), + 411: resources_core.FormatInt64(data.VmDisk), + 412: resources_core.FormatString(data.IpSpaceName), + 413: resources_core.FormatString(data.AccessIpList), + 414: resources_core.FormatString(data.ImageVm), + 415: resources_core.FormatString(data.CloudInit), + 416: resources_core.FormatString(data.UserLogin), + 417: resources_core.FormatString(data.UserPublicKey), + 448: resources_core.FormatString(data.AccessPortList), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 28, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 28, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessIpList", Field: "AccessIpList", Type: "string"}, + {Code: "accessPortList", Field: "AccessPortList", Type: "string"}, + {Code: "cloudInit", Field: "CloudInit", Type: "string"}, + {Code: "imageVm", Field: "ImageVm", Type: "string"}, + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "userLogin", Field: "UserLogin", Type: "string"}, + {Code: "userPublicKey", Field: "UserPublicKey", Type: "string"}, + {Code: "vappUid", Field: "VappUid", Type: "string"}, + {Code: "vmCpu", Field: "VmCpu", Type: "int64"}, + {Code: "vmDisk", Field: "VmDisk", Type: "int64"}, + {Code: "vmName", Field: "VmName", Type: "string"}, + {Code: "vmRam", Field: "VmRam", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.VappUid.IsNull() && !state.VappUid.IsUnknown() { + resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, state.VappUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error()) + } else if resolvedVappUid != "" && resolvedVappUid != state.VappUid.ValueString() { + state.VappUid = types.StringValue(resolvedVappUid) + } + } + // Restore user-provided casing в state (Create). + // EqualFold = «совпадают ли значения без учёта регистра?» + // Если да — значит API вернул «туже» строку, только в другом регистре. + // Заменяем state на original (то что было в plan/config пользователя). + // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint. + if !originalVappUid.IsNull() && !originalVappUid.IsUnknown() && !state.VappUid.IsNull() && !state.VappUid.IsUnknown() { + if strings.EqualFold(state.VappUid.ValueString(), originalVappUid.ValueString()) { + state.VappUid = originalVappUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcVmV3Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VcVmV3Model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 28, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessIpList", Field: "AccessIpList", Type: "string"}, + {Code: "accessPortList", Field: "AccessPortList", Type: "string"}, + {Code: "cloudInit", Field: "CloudInit", Type: "string"}, + {Code: "imageVm", Field: "ImageVm", Type: "string"}, + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "userLogin", Field: "UserLogin", Type: "string"}, + {Code: "userPublicKey", Field: "UserPublicKey", Type: "string"}, + {Code: "vappUid", Field: "VappUid", Type: "string"}, + {Code: "vmCpu", Field: "VmCpu", Type: "int64"}, + {Code: "vmDisk", Field: "VmDisk", Type: "int64"}, + {Code: "vmName", Field: "VmName", Type: "string"}, + {Code: "vmRam", Field: "VmRam", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !newState.VappUid.IsNull() && !newState.VappUid.IsUnknown() { + resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, newState.VappUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error()) + } else if resolvedVappUid != "" && resolvedVappUid != newState.VappUid.ValueString() { + newState.VappUid = types.StringValue(resolvedVappUid) + } + } + // Restore user-provided casing в state (Read). + // При чтении у нас нет plan — но предыдущий state уже хранит значение + // в регистре пользователя (после первого Create оно было восстановлено). + // Берём prior state (переменная state) как эталон регистра. + // Если API вернул то же UUID только строчными буквами — восстанавливаем. + if !state.VappUid.IsNull() && !state.VappUid.IsUnknown() && !newState.VappUid.IsNull() && !newState.VappUid.IsUnknown() { + if strings.EqualFold(newState.VappUid.ValueString(), state.VappUid.ValueString()) { + newState.VappUid = state.VappUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *VcVmV3Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VcVmV3Model + var state VcVmV3Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.VmCpu.IsNull() != state.VmCpu.IsNull() || plan.VmCpu.IsUnknown() != state.VmCpu.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.VmCpu.IsNull() && !plan.VmCpu.IsUnknown() && !state.VmCpu.IsNull() && !state.VmCpu.IsUnknown() { + if plan.VmCpu.ValueInt64() != state.VmCpu.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.VmRam.IsNull() != state.VmRam.IsNull() || plan.VmRam.IsUnknown() != state.VmRam.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.VmRam.IsNull() && !plan.VmRam.IsUnknown() && !state.VmRam.IsNull() && !state.VmRam.IsUnknown() { + if plan.VmRam.ValueInt64() != state.VmRam.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.VmDisk.IsNull() != state.VmDisk.IsNull() || plan.VmDisk.IsUnknown() != state.VmDisk.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.VmDisk.IsNull() && !plan.VmDisk.IsUnknown() && !state.VmDisk.IsNull() && !state.VmDisk.IsUnknown() { + if plan.VmDisk.ValueInt64() != state.VmDisk.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.IpSpaceName.IsNull() != state.IpSpaceName.IsNull() || plan.IpSpaceName.IsUnknown() != state.IpSpaceName.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.IpSpaceName.IsNull() && !plan.IpSpaceName.IsUnknown() && !state.IpSpaceName.IsNull() && !state.IpSpaceName.IsUnknown() { + if !strings.EqualFold(plan.IpSpaceName.ValueString(), state.IpSpaceName.ValueString()) { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.AccessIpList.IsNull() != state.AccessIpList.IsNull() || plan.AccessIpList.IsUnknown() != state.AccessIpList.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.AccessIpList.IsNull() && !plan.AccessIpList.IsUnknown() && !state.AccessIpList.IsNull() && !state.AccessIpList.IsUnknown() { + if !strings.EqualFold(plan.AccessIpList.ValueString(), state.AccessIpList.ValueString()) { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.AccessPortList.IsNull() != state.AccessPortList.IsNull() || plan.AccessPortList.IsUnknown() != state.AccessPortList.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.AccessPortList.IsNull() && !plan.AccessPortList.IsUnknown() && !state.AccessPortList.IsNull() && !state.AccessPortList.IsUnknown() { + if !strings.EqualFold(plan.AccessPortList.ValueString(), state.AccessPortList.ValueString()) { + hasServiceParamChanges = true + } + } + } + // --- Redeploy support (ARCHITECTURE.md) --- + // Проверяем изменился ли git_revision. + // Если да — вызываем redeploy вместо modify. + // git_revision = "" (не задано) → обычный modify. + redeployRequested := false + if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() { + if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() { + redeployRequested = true + } + } + + if !hasServiceParamChanges && !redeployRequested { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + // --- Redeploy support (ARCHITECTURE.md) --- + if redeployRequested { + // Вызываем redeploy — пустой params (операция не требует CFS-параметров) + if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + } else { + params := map[int]string{ + 493: resources_core.FormatInt64(plan.VmCpu), + 494: resources_core.FormatInt64(plan.VmRam), + 495: resources_core.FormatInt64(plan.VmDisk), + 496: resources_core.FormatString(plan.IpSpaceName), + 497: resources_core.FormatString(plan.AccessIpList), + 498: resources_core.FormatString(plan.AccessPortList), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 28, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessIpList", Field: "AccessIpList", Type: "string"}, + {Code: "accessPortList", Field: "AccessPortList", Type: "string"}, + {Code: "cloudInit", Field: "CloudInit", Type: "string"}, + {Code: "imageVm", Field: "ImageVm", Type: "string"}, + {Code: "ipSpaceName", Field: "IpSpaceName", Type: "string"}, + {Code: "userLogin", Field: "UserLogin", Type: "string"}, + {Code: "userPublicKey", Field: "UserPublicKey", Type: "string"}, + {Code: "vappUid", Field: "VappUid", Type: "string"}, + {Code: "vmCpu", Field: "VmCpu", Type: "int64"}, + {Code: "vmDisk", Field: "VmDisk", Type: "int64"}, + {Code: "vmName", Field: "VmName", Type: "string"}, + {Code: "vmRam", Field: "VmRam", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.VappUid.IsNull() && !state.VappUid.IsUnknown() { + resolvedVappUid, err := r.client.ResolveRefSvcParamValue(ctx, 26, state.VappUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vapp_uid", err.Error()) + } else if resolvedVappUid != "" && resolvedVappUid != state.VappUid.ValueString() { + state.VappUid = types.StringValue(resolvedVappUid) + } + } + // Restore user-provided casing в state (Update). + // После API-вызова modify state содержит значения в lower-case от API. + // Plan == config == то что написал пользователь (регистр неизменён). + // EqualFold: если UUID совпадает без учёта регистра — берём из plan. + if !plan.VappUid.IsNull() && !plan.VappUid.IsUnknown() && !state.VappUid.IsNull() && !state.VappUid.IsUnknown() { + if strings.EqualFold(state.VappUid.ValueString(), plan.VappUid.ValueString()) { + state.VappUid = plan.VappUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcVmV3Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VcVmV3Model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *VcVmV3Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *VcVmV3Resource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/29_vc_vdc_group_resource.go b/generated/test/go/29_vc_vdc_group_resource.go new file mode 100644 index 0000000..4166b73 --- /dev/null +++ b/generated/test/go/29_vc_vdc_group_resource.go @@ -0,0 +1,449 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vc_vdc_group +// Service ID: 29 + +var _ resource.Resource = &VcVdcGroupResource{} +var _ resource.ResourceWithModifyPlan = &VcVdcGroupResource{} +var _ resource.ResourceWithImportState = &VcVdcGroupResource{} + +type VcVdcGroupResource struct { + client *core.UniversalClient +} + +type VcVdcGroupModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + VdcUid types.String `tfsdk:"vdc_uid"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewVcVdcGroupResource() resource.Resource { + return &VcVdcGroupResource{} +} + +func (r *VcVdcGroupResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vc_vdc_group" +} + +func (r *VcVdcGroupResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "vdc_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UI display_name for service_id=21 (mapped to UUID in core) Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VcVdcGroupResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *VcVdcGroupModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *VcVdcGroupModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan VcVdcGroupModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan. + // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов. + // Resolve (displayName → UUID или uppercase → lowercase) нужен только для + // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка + // "Provider produced invalid plan: planned value does not match config value". + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.VdcUid.IsNull() && !plan.VdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.VdcUid.ValueString(), state.VdcUid.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить vdc_uid", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.VdcUid.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "vdc_uid is required.") + return + } + if config.VdcUid.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + if !config.VdcUid.IsNull() && !config.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, config.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error()) + } else if resolvedVdcUid != "" && resolvedVdcUid != config.VdcUid.ValueString() { + config.VdcUid = types.StringValue(resolvedVdcUid) + } + } + params := map[int]string{ + 618: resources_core.FormatString(config.VdcUid), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 29, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *VcVdcGroupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data VcVdcGroupModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.VdcUid.IsNull() || data.VdcUid.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "vdc_uid is required.") + return + } + // ══════════════════════════════════════════════════════════════════════════ + // ПРАВИЛО TERRAFORM (официальная документация): + // «If an attribute value is configured, it is NEVER valid to change that + // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что + // написал пользователь). Менять plan запрещено на уровне фреймворка. + // + // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре. + // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный). + // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит: + // «Provider produced inconsistent result after apply». + // + // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот. + // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как + // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API). + // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный + // регистр в state через strings.EqualFold (сравниваем без учёта регистра). + // ══════════════════════════════════════════════════════════════════════════ + originalVdcUid := data.VdcUid + if !data.VdcUid.IsNull() && !data.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, data.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if resolvedVdcUid != "" && resolvedVdcUid != data.VdcUid.ValueString() { + data.VdcUid = types.StringValue(resolvedVdcUid) + } + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 29, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 618: resources_core.FormatString(data.VdcUid), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 29, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 29, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "vdcUid", Field: "VdcUid", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, state.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error()) + } else if resolvedVdcUid != "" && resolvedVdcUid != state.VdcUid.ValueString() { + state.VdcUid = types.StringValue(resolvedVdcUid) + } + } + // Restore user-provided casing в state (Create). + // EqualFold = «совпадают ли значения без учёта регистра?» + // Если да — значит API вернул «туже» строку, только в другом регистре. + // Заменяем state на original (то что было в plan/config пользователя). + // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint. + if !originalVdcUid.IsNull() && !originalVdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + if strings.EqualFold(state.VdcUid.ValueString(), originalVdcUid.ValueString()) { + state.VdcUid = originalVdcUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcVdcGroupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VcVdcGroupModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 29, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "vdcUid", Field: "VdcUid", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !newState.VdcUid.IsNull() && !newState.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, newState.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error()) + } else if resolvedVdcUid != "" && resolvedVdcUid != newState.VdcUid.ValueString() { + newState.VdcUid = types.StringValue(resolvedVdcUid) + } + } + // Restore user-provided casing в state (Read). + // При чтении у нас нет plan — но предыдущий state уже хранит значение + // в регистре пользователя (после первого Create оно было восстановлено). + // Берём prior state (переменная state) как эталон регистра. + // Если API вернул то же UUID только строчными буквами — восстанавливаем. + if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() && !newState.VdcUid.IsNull() && !newState.VdcUid.IsUnknown() { + if strings.EqualFold(newState.VdcUid.ValueString(), state.VdcUid.ValueString()) { + newState.VdcUid = state.VdcUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *VcVdcGroupResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VcVdcGroupModel + var state VcVdcGroupModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 29, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "vdcUid", Field: "VdcUid", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + resolvedVdcUid, err := r.client.ResolveRefSvcParamValue(ctx, 21, state.VdcUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve vdc_uid", err.Error()) + } else if resolvedVdcUid != "" && resolvedVdcUid != state.VdcUid.ValueString() { + state.VdcUid = types.StringValue(resolvedVdcUid) + } + } + // Restore user-provided casing в state (Update). + // После API-вызова modify state содержит значения в lower-case от API. + // Plan == config == то что написал пользователь (регистр неизменён). + // EqualFold: если UUID совпадает без учёта регистра — берём из plan. + if !plan.VdcUid.IsNull() && !plan.VdcUid.IsUnknown() && !state.VdcUid.IsNull() && !state.VdcUid.IsUnknown() { + if strings.EqualFold(state.VdcUid.ValueString(), plan.VdcUid.ValueString()) { + state.VdcUid = plan.VdcUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcVdcGroupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VcVdcGroupModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *VcVdcGroupResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *VcVdcGroupResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/29_vc_vdc_group_vdc_resource.go b/generated/test/go/29_vc_vdc_group_vdc_resource.go new file mode 100644 index 0000000..3e3e8ef --- /dev/null +++ b/generated/test/go/29_vc_vdc_group_vdc_resource.go @@ -0,0 +1,326 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vc_vdc_group +// Subresource: vdc + +var _ resource.Resource = &VcVdcGroupVdcResource{} + +// VcVdcGroupVdcResource manages a subresource via operations. +type VcVdcGroupVdcResource struct { + client *core.UniversalClient +} + +type VcVdcGroupVdcModel struct { + ID types.String `tfsdk:"id"` + VcVdcGroupID types.String `tfsdk:"vc_vdc_group_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` +} + +func NewVcVdcGroupVdcResource() resource.Resource { + return &VcVdcGroupVdcResource{} +} + +func (r *VcVdcGroupVdcResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vc_vdc_group_vdc" +} + +func (r *VcVdcGroupVdcResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "vc_vdc_group_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VcVdcGroupVdcResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan VcVdcGroupVdcModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.VcVdcGroupID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{} + identityCodes := []string{} + listKey, idKey := resources_core.ResolveSubresourceStateKeys("vdc", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "vdc", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{}) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "vdc", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "vdc", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "vdc", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *VcVdcGroupVdcResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VcVdcGroupVdcModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VcVdcGroupVdcResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VcVdcGroupVdcModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state VcVdcGroupVdcModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.VcVdcGroupID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{} + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "vdc", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *VcVdcGroupVdcResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VcVdcGroupVdcModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + return + } + + instanceUID := strings.TrimSpace(state.VcVdcGroupID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{} + identityCodes := []string{} + listKey, idKey := resources_core.ResolveSubresourceStateKeys("vdc", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{}) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *VcVdcGroupVdcResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/2_template_resource.go b/generated/test/go/2_template_resource.go new file mode 100644 index 0000000..b45b9e6 --- /dev/null +++ b/generated/test/go/2_template_resource.go @@ -0,0 +1,397 @@ +package resources_gen + +import ( + "context" + "fmt" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: template +// Service ID: 2 + +var _ resource.Resource = &TemplateResource{} +var _ resource.ResourceWithModifyPlan = &TemplateResource{} +var _ resource.ResourceWithImportState = &TemplateResource{} + +type TemplateResource struct { + client *core.UniversalClient +} + +// TemplateAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration. +type TemplateAccessConfigurationModel struct { + Type types.String `tfsdk:"type" json:"type"` + Domain types.String `tfsdk:"domain" json:"domain"` + MasterIpSpace types.String `tfsdk:"master_ip_space" json:"masterIpSpace"` + MasterAccessList types.String `tfsdk:"master_access_list" json:"masterAccessList"` + SlaveIpSpace types.String `tfsdk:"slave_ip_space" json:"slaveIpSpace"` + SlaveAccessList types.String `tfsdk:"slave_access_list" json:"slaveAccessList"` +} + +// TemplateClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type TemplateClusterConfigurationModel struct { + Replicas types.Int64 `tfsdk:"replicas" json:"replicas"` + Cpu types.Int64 `tfsdk:"cpu" json:"cpu"` + Disk types.Int64 `tfsdk:"disk" json:"disk"` + Memory types.Int64 `tfsdk:"memory" json:"memory"` +} + +// TemplateStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type TemplateStartupConfigurationModel struct { + ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"` +} + +type TemplateModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessConfiguration *TemplateAccessConfigurationModel `tfsdk:"access_configuration"` + ClusterConfiguration *TemplateClusterConfigurationModel `tfsdk:"cluster_configuration"` + StartupConfiguration *TemplateStartupConfigurationModel `tfsdk:"startup_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewTemplateResource() resource.Resource { + return &TemplateResource{} +} + +func (r *TemplateResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_template" +} + +func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "type": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("ingress")}, + "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("abracadabra-pin1v6zw")}, + "master_ip_space": schema.StringAttribute{Required: true}, + "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]")}, + "slave_ip_space": schema.StringAttribute{Required: true}, + "slave_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]")}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)}, + "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100)}, + "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(5)}, + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(128)}, + }}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "resource_realm": schema.StringAttribute{Required: true}, + }}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *TemplateResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *TemplateModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *TemplateModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan TemplateModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 813: resources_core.BuildJSON(map[string]string{"replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64())}), + 814: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}), + 815: resources_core.BuildJSON(map[string]string{"type": fmt.Sprintf("\"%s\"", config.AccessConfiguration.Type.ValueString()), "domain": fmt.Sprintf("\"%s\"", config.AccessConfiguration.Domain.ValueString()), "masterIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.SlaveIpSpace.ValueString()), "slaveAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.SlaveAccessList.ValueString())}), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 2, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *TemplateResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data TemplateModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 2, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 813: resources_core.BuildJSON(map[string]string{"replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64())}), + 814: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}), + 815: resources_core.BuildJSON(map[string]string{"type": fmt.Sprintf("\"%s\"", data.AccessConfiguration.Type.ValueString()), "domain": fmt.Sprintf("\"%s\"", data.AccessConfiguration.Domain.ValueString()), "masterIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.SlaveIpSpace.ValueString()), "slaveAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.SlaveAccessList.ValueString())}), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 2, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 2, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *TemplateResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state TemplateModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 2, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan TemplateModel + var state TemplateModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 816: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64())}), + 817: resources_core.BuildJSON(map[string]string{"type": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.Type.ValueString()), "domain": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.Domain.ValueString()), "masterIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.SlaveIpSpace.ValueString()), "slaveAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.SlaveAccessList.ValueString())}), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 2, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *TemplateResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state TemplateModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *TemplateResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *TemplateResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/32_vmpostgre_resource.go b/generated/test/go/32_vmpostgre_resource.go new file mode 100644 index 0000000..14a037e --- /dev/null +++ b/generated/test/go/32_vmpostgre_resource.go @@ -0,0 +1,322 @@ +package resources_gen + +import ( + "context" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: vmpostgre +// Service ID: 32 + +var _ resource.Resource = &VmpostgreResource{} +var _ resource.ResourceWithModifyPlan = &VmpostgreResource{} +var _ resource.ResourceWithImportState = &VmpostgreResource{} + +type VmpostgreResource struct { + client *core.UniversalClient +} + +type VmpostgreModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewVmpostgreResource() resource.Resource { + return &VmpostgreResource{} +} + +func (r *VmpostgreResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_vmpostgre" +} + +func (r *VmpostgreResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *VmpostgreResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *VmpostgreModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *VmpostgreModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan VmpostgreModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{} + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 32, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *VmpostgreResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data VmpostgreModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 32, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{} + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 32, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 32, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{}) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VmpostgreResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VmpostgreModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 32, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{}) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *VmpostgreResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan VmpostgreModel + var state VmpostgreModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 32, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{}) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VmpostgreResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VmpostgreModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *VmpostgreResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *VmpostgreResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/50_nextcloud_backup_resource.go b/generated/test/go/50_nextcloud_backup_resource.go new file mode 100644 index 0000000..feb1f0e --- /dev/null +++ b/generated/test/go/50_nextcloud_backup_resource.go @@ -0,0 +1,344 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: nextcloud +// Subresource: backup + +var _ resource.Resource = &NextcloudBackupResource{} + +// NextcloudBackupResource manages a subresource via operations. +type NextcloudBackupResource struct { + client *core.UniversalClient +} + +type NextcloudBackupModel struct { + ID types.String `tfsdk:"id"` + NextcloudID types.String `tfsdk:"nextcloud_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + StartupConfiguration types.String `tfsdk:"startup_configuration"` +} + +func NewNextcloudBackupResource() resource.Resource { + return &NextcloudBackupResource{} +} + +func (r *NextcloudBackupResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_nextcloud_backup" +} + +func (r *NextcloudBackupResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "nextcloud_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "startup_configuration": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *NextcloudBackupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan NextcloudBackupModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if plan.StartupConfiguration.IsNull() || plan.StartupConfiguration.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "startup_configuration is required.") + return + } + + instanceUID := strings.TrimSpace(plan.NextcloudID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "startupConfiguration": resources_core.FormatString(plan.StartupConfiguration), + } + identityCodes := []string{ + "startupConfiguration", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("backup", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "startupConfiguration": resources_core.FormatString(plan.StartupConfiguration), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_backup", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_backup", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *NextcloudBackupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state NextcloudBackupModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *NextcloudBackupResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan NextcloudBackupModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state NextcloudBackupModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.NextcloudID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{ + "startupConfiguration": resources_core.FormatString(plan.StartupConfiguration), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *NextcloudBackupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state NextcloudBackupModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + return + } + + instanceUID := strings.TrimSpace(state.NextcloudID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "startupConfiguration": resources_core.FormatString(state.StartupConfiguration), + } + identityCodes := []string{ + "startupConfiguration", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("backup", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{}) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *NextcloudBackupResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/50_nextcloud_resource.go b/generated/test/go/50_nextcloud_resource.go new file mode 100644 index 0000000..43d2c4e --- /dev/null +++ b/generated/test/go/50_nextcloud_resource.go @@ -0,0 +1,414 @@ +package resources_gen + +import ( + "context" + "fmt" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: nextcloud +// Service ID: 50 + +var _ resource.Resource = &NextcloudResource{} +var _ resource.ResourceWithModifyPlan = &NextcloudResource{} +var _ resource.ResourceWithImportState = &NextcloudResource{} + +type NextcloudResource struct { + client *core.UniversalClient +} + +// NextcloudAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration. +type NextcloudAccessConfigurationModel struct { + Domain types.String `tfsdk:"domain" json:"domain"` +} + +// NextcloudAppConfigurationModel — вложенная модель для map-fixed параметра appConfiguration. +type NextcloudAppConfigurationModel struct { + Version types.String `tfsdk:"version" json:"version"` + DocumentServer types.String `tfsdk:"document_server" json:"documentServer"` + LogLevel types.String `tfsdk:"log_level" json:"logLevel"` +} + +// NextcloudClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type NextcloudClusterConfigurationModel struct { + Disk types.Int64 `tfsdk:"disk" json:"disk"` + Cpu types.Int64 `tfsdk:"cpu" json:"cpu"` + Memory types.Int64 `tfsdk:"memory" json:"memory"` + Replicas types.Int64 `tfsdk:"replicas" json:"replicas"` +} + +// NextcloudStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type NextcloudStartupConfigurationModel struct { + PsqlEnabled types.Bool `tfsdk:"psql_enabled" json:"psqlEnabled"` + RedisEnabled types.Bool `tfsdk:"redis_enabled" json:"redisEnabled"` + PsqlUid types.String `tfsdk:"psql_uid" json:"psqlUid"` + RedisUid types.String `tfsdk:"redis_uid" json:"redisUid"` + ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"` +} + +type NextcloudModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessConfiguration *NextcloudAccessConfigurationModel `tfsdk:"access_configuration"` + AppConfiguration *NextcloudAppConfigurationModel `tfsdk:"app_configuration"` + ClusterConfiguration *NextcloudClusterConfigurationModel `tfsdk:"cluster_configuration"` + StartupConfiguration *NextcloudStartupConfigurationModel `tfsdk:"startup_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewNextcloudResource() resource.Resource { + return &NextcloudResource{} +} + +func (r *NextcloudResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_nextcloud" +} + +func (r *NextcloudResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("nextcloud-c2xizgvp"), MarkdownDescription: "Если указан FQDN в формате: `myapp.example.com`, предварительно будет проверяться DNS-запись (А-запись **должна смотреть на IP Ingress кластера ресурсной платформы**)

Если указан в формате `myapp`, DNS-запись будет автоматически создана в служебной DNS-зоне"}, + }}, + "app_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("33.0.4")}, + "document_server": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("collabora"), MarkdownDescription: "Выбор сервера документов (onlyoffice)/(collabora). onlyoffice пока не доступен
Размер пода collabora:
1.8 ядра. 2Gi ОЗУ"}, + "log_level": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("INFO"), MarkdownDescription: "Включает отображение логов самого nextcloud
Логи уходят в /dev/stdout контейнера
`NONE` отключает логирование"}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Указывается в GIGAbytes
На данном диске хранятся данные Nextcloud"}, + "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1000), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"}, + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1024), MarkdownDescription: "Указывается в Megabytes"}, + "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)}, + }}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "psql_enabled": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Инстанс обязан быть развёрнут на той же ресурсной платформе, где и будет располагаться Nextcloud"}, + "redis_enabled": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "Инстанс обязан быть развёрнут на той же ресурсной платформе, где и будет располагаться Nextcloud"}, + "psql_uid": schema.StringAttribute{Required: true, MarkdownDescription: "Необходимо выбирать, когда выбран параметр `Подключить PostgreSQL из Личного кабинета`"}, + "redis_uid": schema.StringAttribute{Required: true, MarkdownDescription: "Необходимо выбирать, когда выбран параметр `Подключить Redis из Личного кабинета`"}, + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"}, + }}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *NextcloudResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *NextcloudModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *NextcloudModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan NextcloudModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 1055: resources_core.BuildJSON(map[string]string{"psqlEnabled": fmt.Sprintf("%v", config.StartupConfiguration.PsqlEnabled.ValueBool()), "redisEnabled": fmt.Sprintf("%v", config.StartupConfiguration.RedisEnabled.ValueBool()), "psqlUid": fmt.Sprintf("\"%s\"", config.StartupConfiguration.PsqlUid.ValueString()), "redisUid": fmt.Sprintf("\"%s\"", config.StartupConfiguration.RedisUid.ValueString()), "resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}), + 1056: resources_core.BuildJSON(map[string]string{"disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64())}), + 1057: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", config.AccessConfiguration.Domain.ValueString())}), + 1058: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.AppConfiguration.Version.ValueString()), "documentServer": fmt.Sprintf("\"%s\"", config.AppConfiguration.DocumentServer.ValueString()), "logLevel": fmt.Sprintf("\"%s\"", config.AppConfiguration.LogLevel.ValueString())}), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 50, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *NextcloudResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data NextcloudModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 50, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 1055: resources_core.BuildJSON(map[string]string{"psqlEnabled": fmt.Sprintf("%v", data.StartupConfiguration.PsqlEnabled.ValueBool()), "redisEnabled": fmt.Sprintf("%v", data.StartupConfiguration.RedisEnabled.ValueBool()), "psqlUid": fmt.Sprintf("\"%s\"", data.StartupConfiguration.PsqlUid.ValueString()), "redisUid": fmt.Sprintf("\"%s\"", data.StartupConfiguration.RedisUid.ValueString()), "resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}), + 1056: resources_core.BuildJSON(map[string]string{"disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64())}), + 1057: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", data.AccessConfiguration.Domain.ValueString())}), + 1058: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.AppConfiguration.Version.ValueString()), "documentServer": fmt.Sprintf("\"%s\"", data.AppConfiguration.DocumentServer.ValueString()), "logLevel": fmt.Sprintf("\"%s\"", data.AppConfiguration.LogLevel.ValueString())}), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 50, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 50, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *NextcloudResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state NextcloudModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 50, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *NextcloudResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan NextcloudModel + var state NextcloudModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 1059: resources_core.BuildJSON(map[string]string{"memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64())}), + 1060: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", plan.AppConfiguration.Version.ValueString()), "documentServer": fmt.Sprintf("\"%s\"", plan.AppConfiguration.DocumentServer.ValueString()), "logLevel": fmt.Sprintf("\"%s\"", plan.AppConfiguration.LogLevel.ValueString())}), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 50, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *NextcloudResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state NextcloudModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *NextcloudResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *NextcloudResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/81_superset_resource.go b/generated/test/go/81_superset_resource.go new file mode 100644 index 0000000..c66e1d8 --- /dev/null +++ b/generated/test/go/81_superset_resource.go @@ -0,0 +1,447 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: superset +// Service ID: 81 + +var _ resource.Resource = &SupersetResource{} +var _ resource.ResourceWithModifyPlan = &SupersetResource{} +var _ resource.ResourceWithImportState = &SupersetResource{} + +type SupersetResource struct { + client *core.UniversalClient +} + +type SupersetModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + Domain types.String `tfsdk:"domain"` + Emails types.String `tfsdk:"emails"` + ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"` + ResourceDisk types.Int64 `tfsdk:"resource_disk"` + ResourceInstances types.Int64 `tfsdk:"resource_instances"` + ResourceMemory types.Int64 `tfsdk:"resource_memory"` + ResourceRealm types.String `tfsdk:"resource_realm"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewSupersetResource() resource.Resource { + return &SupersetResource{} +} + +func (r *SupersetResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_superset" +} + +func (r *SupersetResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("superset-1"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."}, + "emails": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("testuser1"), MarkdownDescription: "Учётная запись, которой будут выданы админские права. В формате email [ну не совсем. Для доменных учетных записей - без доменного суффикса]. Обязана находиться в Keycloak. Можно передавать несколько через пробел Не изменяется после создания."}, + "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Квота ядра пода (Указывается в mili). 1000 Mili = 1 ядро Не изменяется после создания."}, + "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Квота диска (Указывается в GIGAbytes) Не изменяется после создания."}, + "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Количество узлов Не изменяется после создания."}, + "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Квота памяти пода (Указывается в Megabytes) Не изменяется после создания."}, + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *SupersetResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *SupersetModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *SupersetModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan SupersetModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.Emails.IsNull() && !plan.Emails.IsUnknown() && !state.Emails.IsNull() && !state.Emails.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Emails.ValueString(), state.Emails.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить emails", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() { + if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_c_p_u", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() { + if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_disk", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() { + if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_instances", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() { + if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_memory", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.ResourceRealm.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + return + } + if config.ResourceRealm.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 14: resources_core.FormatString(config.Domain), + 20: resources_core.FormatString(config.Emails), + 96: resources_core.FormatInt64(config.ResourceCPU), + 97: resources_core.FormatInt64(config.ResourceMemory), + 98: resources_core.FormatInt64(config.ResourceDisk), + 109: resources_core.FormatString(config.ResourceRealm), + 227: resources_core.FormatInt64(config.ResourceInstances), + } + desiredDomain := "" + if !config.Domain.IsNull() && !config.Domain.IsUnknown() { + desiredDomain = config.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 81, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *SupersetResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data SupersetModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + if !data.Domain.IsNull() && !data.Domain.IsUnknown() { + desiredDomain = data.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 81, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 14: resources_core.FormatString(data.Domain), + 20: resources_core.FormatString(data.Emails), + 96: resources_core.FormatInt64(data.ResourceCPU), + 97: resources_core.FormatInt64(data.ResourceMemory), + 98: resources_core.FormatInt64(data.ResourceDisk), + 109: resources_core.FormatString(data.ResourceRealm), + 227: resources_core.FormatInt64(data.ResourceInstances), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 81, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 81, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "emails", Field: "Emails", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *SupersetResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state SupersetModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 81, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "emails", Field: "Emails", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *SupersetResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan SupersetModel + var state SupersetModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 81, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "emails", Field: "Emails", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *SupersetResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state SupersetModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *SupersetResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *SupersetResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/82_harbor_resource.go b/generated/test/go/82_harbor_resource.go new file mode 100644 index 0000000..ab83b24 --- /dev/null +++ b/generated/test/go/82_harbor_resource.go @@ -0,0 +1,482 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: harbor +// Service ID: 82 + +var _ resource.Resource = &HarborResource{} +var _ resource.ResourceWithModifyPlan = &HarborResource{} +var _ resource.ResourceWithImportState = &HarborResource{} + +type HarborResource struct { + client *core.UniversalClient +} + +type HarborModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + Domain types.String `tfsdk:"domain"` + ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"` + ResourceInstances types.Int64 `tfsdk:"resource_instances"` + ResourceMemory types.Int64 `tfsdk:"resource_memory"` + ResourceRealm types.String `tfsdk:"resource_realm"` + S3Uid types.String `tfsdk:"s3_uid"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewHarborResource() resource.Resource { + return &HarborResource{} +} + +func (r *HarborResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_harbor" +} + +func (r *HarborResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("harbor-1"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."}, + "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100), MarkdownDescription: "Квота ядра пода (Указывается в mili). 1000Mili = 1 Ядро"}, + "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Количество узлов"}, + "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(200), MarkdownDescription: "Квота памяти пода (Указывается в Megabytes)"}, + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + "s3_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UUID экземпляра услуги S3 Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *HarborResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *HarborModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *HarborModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan HarborModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan. + // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов. + // Resolve (displayName → UUID или uppercase → lowercase) нужен только для + // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка + // "Provider produced invalid plan: planned value does not match config value". + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.S3Uid.IsNull() && !plan.S3Uid.IsUnknown() && !state.S3Uid.IsNull() && !state.S3Uid.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.S3Uid.ValueString(), state.S3Uid.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить s3_uid", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.ResourceRealm.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + return + } + if config.ResourceRealm.IsUnknown() { + return + } + if config.S3Uid.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "s3_uid is required.") + return + } + if config.S3Uid.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + if !config.S3Uid.IsNull() && !config.S3Uid.IsUnknown() { + resolvedS3Uid, err := r.client.ResolveRefSvcParamValue(ctx, 12, config.S3Uid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve s3_uid", err.Error()) + } else if resolvedS3Uid != "" && resolvedS3Uid != config.S3Uid.ValueString() { + config.S3Uid = types.StringValue(resolvedS3Uid) + } + } + params := map[int]string{ + 16: resources_core.FormatString(config.Domain), + 110: resources_core.FormatString(config.ResourceRealm), + 228: resources_core.FormatInt64(config.ResourceCPU), + 229: resources_core.FormatInt64(config.ResourceMemory), + 230: resources_core.FormatInt64(config.ResourceInstances), + 231: resources_core.FormatString(config.S3Uid), + } + desiredDomain := "" + if !config.Domain.IsNull() && !config.Domain.IsUnknown() { + desiredDomain = config.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 82, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *HarborResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data HarborModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + return + } + if data.S3Uid.IsNull() || data.S3Uid.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "s3_uid is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + if !data.Domain.IsNull() && !data.Domain.IsUnknown() { + desiredDomain = data.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 82, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 16: resources_core.FormatString(data.Domain), + 110: resources_core.FormatString(data.ResourceRealm), + 228: resources_core.FormatInt64(data.ResourceCPU), + 229: resources_core.FormatInt64(data.ResourceMemory), + 230: resources_core.FormatInt64(data.ResourceInstances), + 231: resources_core.FormatString(data.S3Uid), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 82, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 82, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + // Restore user-provided casing в state (Create). + // EqualFold = «совпадают ли значения без учёта регистра?» + // Если да — значит API вернул «туже» строку, только в другом регистре. + // Заменяем state на original (то что было в plan/config пользователя). + // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint. + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *HarborResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state HarborModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 82, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + // Restore user-provided casing в state (Read). + // При чтении у нас нет plan — но предыдущий state уже хранит значение + // в регистре пользователя (после первого Create оно было восстановлено). + // Берём prior state (переменная state) как эталон регистра. + // Если API вернул то же UUID только строчными буквами — восстанавливаем. + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *HarborResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan HarborModel + var state HarborModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.ResourceInstances.IsNull() != state.ResourceInstances.IsNull() || plan.ResourceInstances.IsUnknown() != state.ResourceInstances.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() { + if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceCPU.IsNull() != state.ResourceCPU.IsNull() || plan.ResourceCPU.IsUnknown() != state.ResourceCPU.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() { + if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceMemory.IsNull() != state.ResourceMemory.IsNull() || plan.ResourceMemory.IsUnknown() != state.ResourceMemory.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() { + if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 822: resources_core.FormatInt64(plan.ResourceInstances), + 823: resources_core.FormatInt64(plan.ResourceCPU), + 824: resources_core.FormatInt64(plan.ResourceMemory), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 82, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + // Restore user-provided casing в state (Update). + // После API-вызова modify state содержит значения в lower-case от API. + // Plan == config == то что написал пользователь (регистр неизменён). + // EqualFold: если UUID совпадает без учёта регистра — берём из plan. + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *HarborResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state HarborModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *HarborResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *HarborResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/86_k8s_velero_resource.go b/generated/test/go/86_k8s_velero_resource.go new file mode 100644 index 0000000..4ad3d14 --- /dev/null +++ b/generated/test/go/86_k8s_velero_resource.go @@ -0,0 +1,401 @@ +package resources_gen + +import ( + "context" + "fmt" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: k8s_velero +// Service ID: 86 + +var _ resource.Resource = &K8sVeleroResource{} +var _ resource.ResourceWithModifyPlan = &K8sVeleroResource{} +var _ resource.ResourceWithImportState = &K8sVeleroResource{} + +type K8sVeleroResource struct { + client *core.UniversalClient +} + +// K8sVeleroBackupConfigurationModel — вложенная модель для map-fixed параметра backupConfiguration. +type K8sVeleroBackupConfigurationModel struct { + S3BucketUid types.String `tfsdk:"s3_bucket_uid" json:"s3BucketUid"` +} + +// K8sVeleroClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type K8sVeleroClusterConfigurationModel struct { + Cpu types.Int64 `tfsdk:"cpu" json:"cpu"` + Memory types.Int64 `tfsdk:"memory" json:"memory"` + Replicas types.Int64 `tfsdk:"replicas" json:"replicas"` +} + +// K8sVeleroNodeAgentConfigurationModel — вложенная модель для map-fixed параметра nodeAgentConfiguration. +type K8sVeleroNodeAgentConfigurationModel struct { + Cpu types.Int64 `tfsdk:"cpu" json:"cpu"` + Memory types.Int64 `tfsdk:"memory" json:"memory"` +} + +// K8sVeleroStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type K8sVeleroStartupConfigurationModel struct { + ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"` +} + +type K8sVeleroModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + BackupConfiguration *K8sVeleroBackupConfigurationModel `tfsdk:"backup_configuration"` + ClusterConfiguration *K8sVeleroClusterConfigurationModel `tfsdk:"cluster_configuration"` + NodeAgentConfiguration *K8sVeleroNodeAgentConfigurationModel `tfsdk:"node_agent_configuration"` + StartupConfiguration *K8sVeleroStartupConfigurationModel `tfsdk:"startup_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewK8sVeleroResource() resource.Resource { + return &K8sVeleroResource{} +} + +func (r *K8sVeleroResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_k8s_velero" +} + +func (r *K8sVeleroResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "backup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "s3_bucket_uid": schema.StringAttribute{Required: true, MarkdownDescription: "Экземпляр S3-Бакета обязателен
Учётная запись будет создана автоматически, если её нету"}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1000), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"}, + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2048), MarkdownDescription: "Указывается в Megabytes"}, + "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)}, + }}, + "node_agent_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2000), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"}, + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(2048), MarkdownDescription: "Указывается в Megabytes"}, + }}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "resource_realm": schema.StringAttribute{Required: true}, + }}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *K8sVeleroResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *K8sVeleroModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *K8sVeleroModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan K8sVeleroModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 962: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64())}), + 963: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}), + 964: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.NodeAgentConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.NodeAgentConfiguration.Memory.ValueInt64())}), + 1076: resources_core.BuildJSON(map[string]string{"s3BucketUid": fmt.Sprintf("\"%s\"", config.BackupConfiguration.S3BucketUid.ValueString())}), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 86, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *K8sVeleroResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data K8sVeleroModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 86, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 962: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64())}), + 963: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}), + 964: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.NodeAgentConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.NodeAgentConfiguration.Memory.ValueInt64())}), + 1076: resources_core.BuildJSON(map[string]string{"s3BucketUid": fmt.Sprintf("\"%s\"", data.BackupConfiguration.S3BucketUid.ValueString())}), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 86, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 86, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "nodeAgentConfiguration", Field: "NodeAgentConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *K8sVeleroResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state K8sVeleroModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 86, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "nodeAgentConfiguration", Field: "NodeAgentConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *K8sVeleroResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan K8sVeleroModel + var state K8sVeleroModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 965: resources_core.BuildJSON(map[string]string{"replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64())}), + 966: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.NodeAgentConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.NodeAgentConfiguration.Memory.ValueInt64())}), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 86, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "nodeAgentConfiguration", Field: "NodeAgentConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *K8sVeleroResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state K8sVeleroModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *K8sVeleroResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *K8sVeleroResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/88_k8s_ziti_controller_resource.go b/generated/test/go/88_k8s_ziti_controller_resource.go new file mode 100644 index 0000000..4c54fce --- /dev/null +++ b/generated/test/go/88_k8s_ziti_controller_resource.go @@ -0,0 +1,431 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: k8s_ziti_controller +// Service ID: 88 + +var _ resource.Resource = &K8sZitiControllerResource{} +var _ resource.ResourceWithModifyPlan = &K8sZitiControllerResource{} +var _ resource.ResourceWithImportState = &K8sZitiControllerResource{} + +type K8sZitiControllerResource struct { + client *core.UniversalClient +} + +type K8sZitiControllerModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + Domain types.String `tfsdk:"domain"` + ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"` + ResourceDisk types.Int64 `tfsdk:"resource_disk"` + ResourceInstances types.Int64 `tfsdk:"resource_instances"` + ResourceMemory types.Int64 `tfsdk:"resource_memory"` + ResourceRealm types.String `tfsdk:"resource_realm"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewK8sZitiControllerResource() resource.Resource { + return &K8sZitiControllerResource{} +} + +func (r *K8sZitiControllerResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_k8s_ziti_controller" +} + +func (r *K8sZitiControllerResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("k8sZitiController-1"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."}, + "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(300), MarkdownDescription: "Квота ядра пода (milicores) Не изменяется после создания."}, + "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Квота диска (Указывается в GIGAbytes) Не изменяется после создания."}, + "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Количество узлов Не изменяется после создания."}, + "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Квота памяти пода (MBytes) Не изменяется после создания."}, + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *K8sZitiControllerResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *K8sZitiControllerModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *K8sZitiControllerModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan K8sZitiControllerModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() { + if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_c_p_u", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() { + if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_disk", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() { + if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_instances", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() { + if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_memory", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.ResourceRealm.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + return + } + if config.ResourceRealm.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 294: resources_core.FormatInt64(config.ResourceCPU), + 295: resources_core.FormatInt64(config.ResourceMemory), + 296: resources_core.FormatInt64(config.ResourceDisk), + 297: resources_core.FormatInt64(config.ResourceInstances), + 298: resources_core.FormatString(config.ResourceRealm), + 299: resources_core.FormatString(config.Domain), + } + desiredDomain := "" + if !config.Domain.IsNull() && !config.Domain.IsUnknown() { + desiredDomain = config.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 88, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *K8sZitiControllerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data K8sZitiControllerModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + if !data.Domain.IsNull() && !data.Domain.IsUnknown() { + desiredDomain = data.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 88, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 294: resources_core.FormatInt64(data.ResourceCPU), + 295: resources_core.FormatInt64(data.ResourceMemory), + 296: resources_core.FormatInt64(data.ResourceDisk), + 297: resources_core.FormatInt64(data.ResourceInstances), + 298: resources_core.FormatString(data.ResourceRealm), + 299: resources_core.FormatString(data.Domain), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 88, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 88, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *K8sZitiControllerResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state K8sZitiControllerModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 88, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *K8sZitiControllerResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan K8sZitiControllerModel + var state K8sZitiControllerModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 88, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *K8sZitiControllerResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state K8sZitiControllerModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *K8sZitiControllerResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *K8sZitiControllerResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/89_flask_resource.go b/generated/test/go/89_flask_resource.go new file mode 100644 index 0000000..1fe8642 --- /dev/null +++ b/generated/test/go/89_flask_resource.go @@ -0,0 +1,449 @@ +package resources_gen + +import ( + "context" + "fmt" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: flask +// Service ID: 89 + +var _ resource.Resource = &FlaskResource{} +var _ resource.ResourceWithModifyPlan = &FlaskResource{} +var _ resource.ResourceWithImportState = &FlaskResource{} + +type FlaskResource struct { + client *core.UniversalClient +} + +// FlaskAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration. +type FlaskAccessConfigurationModel struct { + Domain types.String `tfsdk:"domain" json:"domain"` +} + +// FlaskAppConfigurationModel — вложенная модель для map-fixed параметра appConfiguration. +type FlaskAppConfigurationModel struct { + Version types.String `tfsdk:"version" json:"version"` + GitPath types.String `tfsdk:"git_path" json:"gitPath"` + HealthPath types.String `tfsdk:"health_path" json:"healthPath"` +} + +// FlaskClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type FlaskClusterConfigurationModel struct { + Cpu types.Int64 `tfsdk:"cpu" json:"cpu"` + Memory types.Int64 `tfsdk:"memory" json:"memory"` + Replicas types.Int64 `tfsdk:"replicas" json:"replicas"` +} + +// FlaskStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type FlaskStartupConfigurationModel struct { + ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"` +} + +type FlaskModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + // --- Redeploy support (ARCHITECTURE.md) --- + // git_revision: при изменении вызывает redeploy вместо modify. + // Опциональное поле — если не задано, modify работает как обычно. + GitRevision types.String `tfsdk:"git_revision"` + AccessConfiguration *FlaskAccessConfigurationModel `tfsdk:"access_configuration"` + AppConfiguration *FlaskAppConfigurationModel `tfsdk:"app_configuration"` + ClusterConfiguration *FlaskClusterConfigurationModel `tfsdk:"cluster_configuration"` + JsonEnv types.String `tfsdk:"json_env"` + StartupConfiguration *FlaskStartupConfigurationModel `tfsdk:"startup_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewFlaskResource() resource.Resource { + return &FlaskResource{} +} + +func (r *FlaskResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_flask" +} + +func (r *FlaskResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("flask-mdpw2bcy"), MarkdownDescription: "Если указан FQDN в формате: `myapp.example.com`, предварительно будет проверяться DNS-запись (А-запись **должна смотреть на IP Ingress кластера ресурсной платформы**)

Если указан в формате `myapp`, DNS-запись будет автоматически создана в служебной DNS-зоне"}, + }}, + "app_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("3.12")}, + "git_path": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("https://github.com/Foxyhhd/Baldurs-Gate-test.git"), MarkdownDescription: "Путь до репозитория приложения
Обязательно должен быть передан как URL: `https://....`

Должен быть сетевой доступ до URL для скачивания исходников"}, + "health_path": schema.StringAttribute{Optional: true, MarkdownDescription: "Путь до healtcheck для запуска пода в Kubernetes, который отдаст 200 код
Пустое значение означает, что будет проверяться исключительно доступность приложения по порту"}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(200), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"}, + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(256), MarkdownDescription: "Указывается в Megabytes"}, + "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)}, + }}, + "json_env": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("{}")}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"}, + }}, + // --- Redeploy support (ARCHITECTURE.md) --- + // При изменении git_revision вызывается redeploy вместо modify. + // Если не задано — modify работает без изменений. + "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *FlaskResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *FlaskModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *FlaskModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan FlaskModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 1037: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}), + 1038: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64())}), + 1039: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", config.AccessConfiguration.Domain.ValueString())}), + 1040: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", config.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", config.AppConfiguration.HealthPath.ValueString())}), + 1041: resources_core.FormatString(config.JsonEnv), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 89, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *FlaskResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data FlaskModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 89, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 1037: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}), + 1038: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64())}), + 1039: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", data.AccessConfiguration.Domain.ValueString())}), + 1040: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", data.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", data.AppConfiguration.HealthPath.ValueString())}), + 1041: resources_core.FormatString(data.JsonEnv), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 89, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 89, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "jsonEnv", Field: "JsonEnv", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *FlaskResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state FlaskModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 89, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "jsonEnv", Field: "JsonEnv", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *FlaskResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan FlaskModel + var state FlaskModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.JsonEnv.IsNull() != state.JsonEnv.IsNull() || plan.JsonEnv.IsUnknown() != state.JsonEnv.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.JsonEnv.IsNull() && !plan.JsonEnv.IsUnknown() && !state.JsonEnv.IsNull() && !state.JsonEnv.IsUnknown() { + if !strings.EqualFold(plan.JsonEnv.ValueString(), state.JsonEnv.ValueString()) { + hasServiceParamChanges = true + } + } + } + // --- Redeploy support (ARCHITECTURE.md) --- + // Проверяем изменился ли git_revision. + // Если да — вызываем redeploy вместо modify. + // git_revision = "" (не задано) → обычный modify. + redeployRequested := false + if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() { + if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() { + redeployRequested = true + } + } + + if !hasServiceParamChanges && !redeployRequested { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + // --- Redeploy support (ARCHITECTURE.md) --- + if redeployRequested { + // Вызываем redeploy — пустой params (операция не требует CFS-параметров) + if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + } else { + params := map[int]string{ + 1042: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64())}), + 1043: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", plan.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", plan.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", plan.AppConfiguration.HealthPath.ValueString())}), + 1044: resources_core.FormatString(plan.JsonEnv), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 89, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "jsonEnv", Field: "JsonEnv", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *FlaskResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state FlaskModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *FlaskResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *FlaskResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/90_postgres_backup_resource.go b/generated/test/go/90_postgres_backup_resource.go new file mode 100644 index 0000000..957eb3e --- /dev/null +++ b/generated/test/go/90_postgres_backup_resource.go @@ -0,0 +1,326 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: postgres +// Subresource: backup + +var _ resource.Resource = &PostgresBackupResource{} + +// PostgresBackupResource manages a subresource via operations. +type PostgresBackupResource struct { + client *core.UniversalClient +} + +type PostgresBackupModel struct { + ID types.String `tfsdk:"id"` + PostgresID types.String `tfsdk:"postgres_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` +} + +func NewPostgresBackupResource() resource.Resource { + return &PostgresBackupResource{} +} + +func (r *PostgresBackupResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_postgres_backup" +} + +func (r *PostgresBackupResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "postgres_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *PostgresBackupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan PostgresBackupModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.PostgresID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{} + identityCodes := []string{} + listKey, idKey := resources_core.ResolveSubresourceStateKeys("backup", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{}) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_backup", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_backup", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *PostgresBackupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state PostgresBackupModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *PostgresBackupResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan PostgresBackupModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state PostgresBackupModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.PostgresID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{} + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *PostgresBackupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state PostgresBackupModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + return + } + + instanceUID := strings.TrimSpace(state.PostgresID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{} + identityCodes := []string{} + listKey, idKey := resources_core.ResolveSubresourceStateKeys("backup", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{}) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *PostgresBackupResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/90_postgres_database_resource.go b/generated/test/go/90_postgres_database_resource.go new file mode 100644 index 0000000..7310720 --- /dev/null +++ b/generated/test/go/90_postgres_database_resource.go @@ -0,0 +1,358 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: postgres +// Subresource: database + +var _ resource.Resource = &PostgresDatabaseResource{} + +// PostgresDatabaseResource manages a subresource via operations. +type PostgresDatabaseResource struct { + client *core.UniversalClient +} + +type PostgresDatabaseModel struct { + ID types.String `tfsdk:"id"` + PostgresID types.String `tfsdk:"postgres_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + DbName types.String `tfsdk:"db_name"` + DbOwner types.String `tfsdk:"db_owner"` +} + +func NewPostgresDatabaseResource() resource.Resource { + return &PostgresDatabaseResource{} +} + +func (r *PostgresDatabaseResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_postgres_database" +} + +func (r *PostgresDatabaseResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "postgres_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "db_name": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "db_owner": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *PostgresDatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan PostgresDatabaseModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if plan.DbName.IsNull() || plan.DbName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "db_name is required.") + return + } + if plan.DbOwner.IsNull() || plan.DbOwner.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "db_owner is required.") + return + } + + instanceUID := strings.TrimSpace(plan.PostgresID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "dbName": resources_core.FormatString(plan.DbName), + "dbOwner": resources_core.FormatString(plan.DbOwner), + } + identityCodes := []string{ + "dbName", + "dbOwner", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("database", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "dbName": resources_core.FormatString(plan.DbName), + "dbOwner": resources_core.FormatString(plan.DbOwner), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_database", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_database", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *PostgresDatabaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state PostgresDatabaseModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *PostgresDatabaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan PostgresDatabaseModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state PostgresDatabaseModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.PostgresID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{ + "dbName": resources_core.FormatString(plan.DbName), + "dbOwner": resources_core.FormatString(plan.DbOwner), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "database", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *PostgresDatabaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state PostgresDatabaseModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_database" == "" { + return + } + + instanceUID := strings.TrimSpace(state.PostgresID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "dbName": resources_core.FormatString(state.DbName), + "dbOwner": resources_core.FormatString(state.DbOwner), + } + identityCodes := []string{ + "dbName", + "dbOwner", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("database", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "dbName": resources_core.FormatString(state.DbName), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_database", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *PostgresDatabaseResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/90_postgres_resource.go b/generated/test/go/90_postgres_resource.go new file mode 100644 index 0000000..4b3c2ca --- /dev/null +++ b/generated/test/go/90_postgres_resource.go @@ -0,0 +1,475 @@ +package resources_gen + +import ( + "context" + "fmt" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: postgres +// Service ID: 90 + +var _ resource.Resource = &PostgresResource{} +var _ resource.ResourceWithModifyPlan = &PostgresResource{} +var _ resource.ResourceWithImportState = &PostgresResource{} + +type PostgresResource struct { + client *core.UniversalClient +} + +// PostgresAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration. +type PostgresAccessConfigurationModel struct { + MasterAccessList types.String `tfsdk:"master_access_list" json:"masterAccessList"` + SlaveIpSpace types.String `tfsdk:"slave_ip_space" json:"slaveIpSpace"` + SlaveAccessList types.String `tfsdk:"slave_access_list" json:"slaveAccessList"` + MasterIpSpace types.String `tfsdk:"master_ip_space" json:"masterIpSpace"` +} + +// PostgresAutoscaleConfigurationModel — вложенная модель для map-fixed параметра autoscaleConfiguration. +type PostgresAutoscaleConfigurationModel struct { + Enabled types.Bool `tfsdk:"enabled" json:"enabled"` + Schedule types.Int64 `tfsdk:"schedule" json:"schedule"` + Quota types.Int64 `tfsdk:"quota" json:"quota"` + Percent types.Int64 `tfsdk:"percent" json:"percent"` +} + +// PostgresBackupConfigurationModel — вложенная модель для map-fixed параметра backupConfiguration. +type PostgresBackupConfigurationModel struct { + Schedule types.String `tfsdk:"schedule" json:"schedule"` + S3Uid types.String `tfsdk:"s3_uid" json:"s3Uid"` + Retain types.Int64 `tfsdk:"retain" json:"retain"` +} + +// PostgresClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type PostgresClusterConfigurationModel struct { + Cpu types.Int64 `tfsdk:"cpu" json:"cpu"` + Memory types.Int64 `tfsdk:"memory" json:"memory"` + Replicas types.Int64 `tfsdk:"replicas" json:"replicas"` + Disk types.Int64 `tfsdk:"disk" json:"disk"` +} + +// PostgresPostgresConfigurationModel — вложенная модель для map-fixed параметра postgresConfiguration. +type PostgresPostgresConfigurationModel struct { + Version types.String `tfsdk:"version" json:"version"` + SslRequired types.Bool `tfsdk:"ssl_required" json:"sslRequired"` + PoolerMaster types.Bool `tfsdk:"pooler_master" json:"poolerMaster"` + PoolerSlave types.Bool `tfsdk:"pooler_slave" json:"poolerSlave"` +} + +// PostgresStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type PostgresStartupConfigurationModel struct { + ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"` +} + +type PostgresModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessConfiguration *PostgresAccessConfigurationModel `tfsdk:"access_configuration"` + AutoscaleConfiguration *PostgresAutoscaleConfigurationModel `tfsdk:"autoscale_configuration"` + BackupConfiguration *PostgresBackupConfigurationModel `tfsdk:"backup_configuration"` + ClusterConfiguration *PostgresClusterConfigurationModel `tfsdk:"cluster_configuration"` + PostgresConf types.String `tfsdk:"postgres_conf"` + PostgresConfiguration *PostgresPostgresConfigurationModel `tfsdk:"postgres_configuration"` + StartupConfiguration *PostgresStartupConfigurationModel `tfsdk:"startup_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewPostgresResource() resource.Resource { + return &PostgresResource{} +} + +func (r *PostgresResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_postgres" +} + +func (r *PostgresResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"}, + "slave_ip_space": schema.StringAttribute{Required: true, MarkdownDescription: "Из какого Ip-Space резервировать внешний IP
Список предоставляется из Организации, в котором развёрнута ресурсная платформа"}, + "slave_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"}, + "master_ip_space": schema.StringAttribute{Required: true, MarkdownDescription: "Из какого Ip-Space резервировать внешний IP
Список предоставляется из Организации, в котором развёрнута ресурсная платформа"}, + }}, + "autoscale_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "enabled": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включает автоскейлинг PV. При включении параметра необходимо настроить autoScalePercentage и autoScaleTechWindow. Алерт при срабатывании вызывает modify у текущей услуги"}, + "schedule": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(0), MarkdownDescription: "Требует включения autoScale. Окно обновления (пока не реализовано). Указывается час в виде Integer (0 / 5 / 12 / 23)"}, + "quota": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100), MarkdownDescription: "До какого размера возможно увеличивать диск, когда включен Autoscale"}, + "percent": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Требует включения autoScale. Позволяет указать в процентах расширение от текущего макс объема
Минимальный размер на который расширяется - **1Gb**"}, + }}, + "backup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "schedule": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("0 0 * * *")}, + "s3_uid": schema.StringAttribute{Required: true, MarkdownDescription: "Экземпляр S3 обязателен"}, + "retain": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(7)}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"}, + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Указывается в Megabytes"}, + "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)}, + "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Указывается в GIGAbytes"}, + }}, + "postgres_conf": schema.StringAttribute{Optional: true}, + "postgres_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("17")}, + "ssl_required": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "По умолчанию подключение обязательное (sslmode=require)"}, + "pooler_master": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включает pgPooler (на запись). Будет создан новый под
Размер пода **200m/400Mi**"}, + "pooler_slave": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Включает pgPooler (на чтение). Будет создан новый под
Размер пода **200m/400Mi**"}, + }}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"}, + }}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *PostgresResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *PostgresModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *PostgresModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan PostgresModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 788: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64())}), + 789: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}), + 790: resources_core.BuildJSON(map[string]string{"masterAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.SlaveIpSpace.ValueString()), "slaveAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.SlaveAccessList.ValueString()), "masterIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterIpSpace.ValueString())}), + 791: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.PostgresConfiguration.Version.ValueString()), "sslRequired": fmt.Sprintf("%v", config.PostgresConfiguration.SslRequired.ValueBool()), "poolerMaster": fmt.Sprintf("%v", config.PostgresConfiguration.PoolerMaster.ValueBool()), "poolerSlave": fmt.Sprintf("%v", config.PostgresConfiguration.PoolerSlave.ValueBool())}), + 792: resources_core.FormatString(config.PostgresConf), + 793: resources_core.BuildJSON(map[string]string{"schedule": fmt.Sprintf("\"%s\"", config.BackupConfiguration.Schedule.ValueString()), "s3Uid": fmt.Sprintf("\"%s\"", config.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", config.BackupConfiguration.Retain.ValueInt64())}), + 794: resources_core.BuildJSON(map[string]string{"enabled": fmt.Sprintf("%v", config.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", config.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", config.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", config.AutoscaleConfiguration.Percent.ValueInt64())}), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 90, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *PostgresResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data PostgresModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 90, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 788: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64())}), + 789: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}), + 790: resources_core.BuildJSON(map[string]string{"masterAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.SlaveIpSpace.ValueString()), "slaveAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.SlaveAccessList.ValueString()), "masterIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterIpSpace.ValueString())}), + 791: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.PostgresConfiguration.Version.ValueString()), "sslRequired": fmt.Sprintf("%v", data.PostgresConfiguration.SslRequired.ValueBool()), "poolerMaster": fmt.Sprintf("%v", data.PostgresConfiguration.PoolerMaster.ValueBool()), "poolerSlave": fmt.Sprintf("%v", data.PostgresConfiguration.PoolerSlave.ValueBool())}), + 792: resources_core.FormatString(data.PostgresConf), + 793: resources_core.BuildJSON(map[string]string{"schedule": fmt.Sprintf("\"%s\"", data.BackupConfiguration.Schedule.ValueString()), "s3Uid": fmt.Sprintf("\"%s\"", data.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", data.BackupConfiguration.Retain.ValueInt64())}), + 794: resources_core.BuildJSON(map[string]string{"enabled": fmt.Sprintf("%v", data.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", data.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", data.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", data.AutoscaleConfiguration.Percent.ValueInt64())}), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 90, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 90, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"}, + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "postgresConf", Field: "PostgresConf", Type: "array-map-fixed"}, + {Code: "postgresConfiguration", Field: "PostgresConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *PostgresResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state PostgresModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 90, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"}, + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "postgresConf", Field: "PostgresConf", Type: "array-map-fixed"}, + {Code: "postgresConfiguration", Field: "PostgresConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *PostgresResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan PostgresModel + var state PostgresModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.PostgresConf.IsNull() != state.PostgresConf.IsNull() || plan.PostgresConf.IsUnknown() != state.PostgresConf.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.PostgresConf.IsNull() && !plan.PostgresConf.IsUnknown() && !state.PostgresConf.IsNull() && !state.PostgresConf.IsUnknown() { + if !strings.EqualFold(plan.PostgresConf.ValueString(), state.PostgresConf.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 795: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64())}), + 796: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.SlaveIpSpace.ValueString()), "slaveAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.SlaveAccessList.ValueString())}), + 797: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", plan.PostgresConfiguration.Version.ValueString()), "sslRequired": fmt.Sprintf("%v", plan.PostgresConfiguration.SslRequired.ValueBool()), "poolerMaster": fmt.Sprintf("%v", plan.PostgresConfiguration.PoolerMaster.ValueBool()), "poolerSlave": fmt.Sprintf("%v", plan.PostgresConfiguration.PoolerSlave.ValueBool())}), + 798: resources_core.FormatString(plan.PostgresConf), + 799: resources_core.BuildJSON(map[string]string{"s3Uid": fmt.Sprintf("\"%s\"", plan.BackupConfiguration.S3Uid.ValueString()), "retain": fmt.Sprintf("%d", plan.BackupConfiguration.Retain.ValueInt64()), "schedule": fmt.Sprintf("\"%s\"", plan.BackupConfiguration.Schedule.ValueString())}), + 800: resources_core.BuildJSON(map[string]string{"enabled": fmt.Sprintf("%v", plan.AutoscaleConfiguration.Enabled.ValueBool()), "schedule": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Schedule.ValueInt64()), "quota": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Quota.ValueInt64()), "percent": fmt.Sprintf("%d", plan.AutoscaleConfiguration.Percent.ValueInt64())}), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 90, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "autoscaleConfiguration", Field: "AutoscaleConfiguration", Type: "map-fixed"}, + {Code: "backupConfiguration", Field: "BackupConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "postgresConf", Field: "PostgresConf", Type: "array-map-fixed"}, + {Code: "postgresConfiguration", Field: "PostgresConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *PostgresResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state PostgresModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *PostgresResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *PostgresResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/90_postgres_user_resource.go b/generated/test/go/90_postgres_user_resource.go new file mode 100644 index 0000000..5101eca --- /dev/null +++ b/generated/test/go/90_postgres_user_resource.go @@ -0,0 +1,358 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: postgres +// Subresource: user + +var _ resource.Resource = &PostgresUserResource{} + +// PostgresUserResource manages a subresource via operations. +type PostgresUserResource struct { + client *core.UniversalClient +} + +type PostgresUserModel struct { + ID types.String `tfsdk:"id"` + PostgresID types.String `tfsdk:"postgres_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + Role types.String `tfsdk:"role"` + Username types.String `tfsdk:"username"` +} + +func NewPostgresUserResource() resource.Resource { + return &PostgresUserResource{} +} + +func (r *PostgresUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_postgres_user" +} + +func (r *PostgresUserResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "postgres_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "role": schema.StringAttribute{Required: true, MarkdownDescription: "Выбрать роль из списка Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + "username": schema.StringAttribute{Required: true, MarkdownDescription: "Не изменяется после создания. Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *PostgresUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan PostgresUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if plan.Role.IsNull() || plan.Role.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "role is required.") + return + } + if plan.Username.IsNull() || plan.Username.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "username is required.") + return + } + + instanceUID := strings.TrimSpace(plan.PostgresID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "role": resources_core.FormatString(plan.Role), + "username": resources_core.FormatString(plan.Username), + } + identityCodes := []string{ + "role", + "username", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "username": resources_core.FormatString(plan.Username), + "role": resources_core.FormatString(plan.Role), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *PostgresUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state PostgresUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *PostgresUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan PostgresUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state PostgresUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.PostgresID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{ + "role": resources_core.FormatString(plan.Role), + "username": resources_core.FormatString(plan.Username), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *PostgresUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state PostgresUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_user" == "" { + return + } + + instanceUID := strings.TrimSpace(state.PostgresID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "role": resources_core.FormatString(state.Role), + "username": resources_core.FormatString(state.Username), + } + identityCodes := []string{ + "role", + "username", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "username": resources_core.FormatString(state.Username), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_user", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *PostgresUserResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/91_redis_resource.go b/generated/test/go/91_redis_resource.go new file mode 100644 index 0000000..864cd27 --- /dev/null +++ b/generated/test/go/91_redis_resource.go @@ -0,0 +1,393 @@ +package resources_gen + +import ( + "context" + "fmt" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: redis +// Service ID: 91 + +var _ resource.Resource = &RedisResource{} +var _ resource.ResourceWithModifyPlan = &RedisResource{} +var _ resource.ResourceWithImportState = &RedisResource{} + +type RedisResource struct { + client *core.UniversalClient +} + +// RedisAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration. +type RedisAccessConfigurationModel struct { + SlaveAccessList types.String `tfsdk:"slave_access_list" json:"slaveAccessList"` + MasterIpSpace types.String `tfsdk:"master_ip_space" json:"masterIpSpace"` + MasterAccessList types.String `tfsdk:"master_access_list" json:"masterAccessList"` + SlaveIpSpace types.String `tfsdk:"slave_ip_space" json:"slaveIpSpace"` +} + +// RedisClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type RedisClusterConfigurationModel struct { + Cpu types.Int64 `tfsdk:"cpu" json:"cpu"` + Memory types.Int64 `tfsdk:"memory" json:"memory"` + Replicas types.Int64 `tfsdk:"replicas" json:"replicas"` + Disk types.Int64 `tfsdk:"disk" json:"disk"` +} + +// RedisStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type RedisStartupConfigurationModel struct { + ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"` +} + +type RedisModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessConfiguration *RedisAccessConfigurationModel `tfsdk:"access_configuration"` + ClusterConfiguration *RedisClusterConfigurationModel `tfsdk:"cluster_configuration"` + StartupConfiguration *RedisStartupConfigurationModel `tfsdk:"startup_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewRedisResource() resource.Resource { + return &RedisResource{} +} + +func (r *RedisResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_redis" +} + +func (r *RedisResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "slave_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"}, + "master_ip_space": schema.StringAttribute{Required: true, MarkdownDescription: "Из какого Ip-Space резервировать внешний IP
Список предоставляется из Организации, в котором развёрнута ресурсная платформа"}, + "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"}, + "slave_ip_space": schema.StringAttribute{Required: true, MarkdownDescription: "Из какого Ip-Space резервировать внешний IP
Список предоставляется из Организации, в котором развёрнута ресурсная платформа"}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"}, + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Указывается в Megabytes"}, + "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)}, + "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(10), MarkdownDescription: "Указывается в GIGAbytes"}, + }}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"}, + }}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *RedisResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *RedisModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *RedisModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan RedisModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 1061: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}), + 1062: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64())}), + 1063: resources_core.BuildJSON(map[string]string{"slaveAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.SlaveAccessList.ValueString()), "masterIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.SlaveIpSpace.ValueString())}), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 91, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *RedisResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data RedisModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 91, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 1061: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}), + 1062: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64())}), + 1063: resources_core.BuildJSON(map[string]string{"slaveAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.SlaveAccessList.ValueString()), "masterIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.SlaveIpSpace.ValueString())}), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 91, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 91, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *RedisResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state RedisModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 91, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *RedisResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan RedisModel + var state RedisModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 1064: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64())}), + 1065: resources_core.BuildJSON(map[string]string{"slaveAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.SlaveAccessList.ValueString()), "masterIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterAccessList.ValueString()), "slaveIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.SlaveIpSpace.ValueString())}), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 91, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *RedisResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state RedisModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *RedisResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *RedisResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/92_mongodb_resource.go b/generated/test/go/92_mongodb_resource.go new file mode 100644 index 0000000..d4f5b8a --- /dev/null +++ b/generated/test/go/92_mongodb_resource.go @@ -0,0 +1,389 @@ +package resources_gen + +import ( + "context" + "fmt" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: mongodb +// Service ID: 92 + +var _ resource.Resource = &MongodbResource{} +var _ resource.ResourceWithModifyPlan = &MongodbResource{} +var _ resource.ResourceWithImportState = &MongodbResource{} + +type MongodbResource struct { + client *core.UniversalClient +} + +// MongodbAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration. +type MongodbAccessConfigurationModel struct { + MasterIpSpace types.String `tfsdk:"master_ip_space" json:"masterIpSpace"` + MasterAccessList types.String `tfsdk:"master_access_list" json:"masterAccessList"` +} + +// MongodbClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type MongodbClusterConfigurationModel struct { + Memory types.Int64 `tfsdk:"memory" json:"memory"` + Replicas types.Int64 `tfsdk:"replicas" json:"replicas"` + Disk types.Int64 `tfsdk:"disk" json:"disk"` + Cpu types.Int64 `tfsdk:"cpu" json:"cpu"` +} + +// MongodbStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type MongodbStartupConfigurationModel struct { + ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"` +} + +type MongodbModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + AccessConfiguration *MongodbAccessConfigurationModel `tfsdk:"access_configuration"` + ClusterConfiguration *MongodbClusterConfigurationModel `tfsdk:"cluster_configuration"` + StartupConfiguration *MongodbStartupConfigurationModel `tfsdk:"startup_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewMongodbResource() resource.Resource { + return &MongodbResource{} +} + +func (r *MongodbResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_mongodb" +} + +func (r *MongodbResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "master_ip_space": schema.StringAttribute{Required: true, MarkdownDescription: "Из какого Ip-Space резервировать внешний IP
Список предоставляется из Организации, в котором развёрнута ресурсная платформа"}, + "master_access_list": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("[]"), MarkdownDescription: "Необходимо настраивать, когда зарезервирован внешний адрес
Если передан пустой массив, доступ выделяется всем"}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1024)}, + "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)}, + "disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(5), MarkdownDescription: "Указывается в Gb"}, + "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1000)}, + }}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"}, + }}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *MongodbResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *MongodbModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *MongodbModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan MongodbModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 1089: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}), + 1090: resources_core.BuildJSON(map[string]string{"memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", config.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64())}), + 1091: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", config.AccessConfiguration.MasterAccessList.ValueString())}), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 92, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *MongodbResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data MongodbModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 92, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 1089: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}), + 1090: resources_core.BuildJSON(map[string]string{"memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64()), "disk": fmt.Sprintf("%d", data.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64())}), + 1091: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", data.AccessConfiguration.MasterAccessList.ValueString())}), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 92, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 92, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *MongodbResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state MongodbModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 92, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *MongodbResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan MongodbModel + var state MongodbModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 1092: resources_core.BuildJSON(map[string]string{"disk": fmt.Sprintf("%d", plan.ClusterConfiguration.Disk.ValueInt64()), "cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64())}), + 1093: resources_core.BuildJSON(map[string]string{"masterIpSpace": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterIpSpace.ValueString()), "masterAccessList": fmt.Sprintf("\"%s\"", plan.AccessConfiguration.MasterAccessList.ValueString())}), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 92, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *MongodbResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state MongodbModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *MongodbResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *MongodbResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/92_mongodb_user_resource.go b/generated/test/go/92_mongodb_user_resource.go new file mode 100644 index 0000000..6f0e40d --- /dev/null +++ b/generated/test/go/92_mongodb_user_resource.go @@ -0,0 +1,364 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: mongodb +// Subresource: user + +var _ resource.Resource = &MongodbUserResource{} + +// MongodbUserResource manages a subresource via operations. +type MongodbUserResource struct { + client *core.UniversalClient +} + +type MongodbUserModel struct { + ID types.String `tfsdk:"id"` + MongodbID types.String `tfsdk:"mongodb_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + DbName types.String `tfsdk:"db_name"` + Role types.String `tfsdk:"role"` + Username types.String `tfsdk:"username"` +} + +func NewMongodbUserResource() resource.Resource { + return &MongodbUserResource{} +} + +func (r *MongodbUserResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_mongodb_user" +} + +func (r *MongodbUserResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "mongodb_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "db_name": schema.StringAttribute{Required: true}, + "role": schema.StringAttribute{Required: true}, + "username": schema.StringAttribute{Required: true, MarkdownDescription: "Изменение требует пересоздания.", PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *MongodbUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan MongodbUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if plan.DbName.IsNull() || plan.DbName.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "db_name is required.") + return + } + if plan.Role.IsNull() || plan.Role.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "role is required.") + return + } + if plan.Username.IsNull() || plan.Username.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "username is required.") + return + } + + instanceUID := strings.TrimSpace(plan.MongodbID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{ + "username": resources_core.FormatString(plan.Username), + } + identityCodes := []string{ + "username", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "username": resources_core.FormatString(plan.Username), + "dbName": resources_core.FormatString(plan.DbName), + "role": resources_core.FormatString(plan.Role), + }) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_user", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if found || !known { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *MongodbUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state MongodbUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *MongodbUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan MongodbUserModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "modify_user" == "" { + var state MongodbUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.MongodbID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{ + "username": resources_core.FormatString(plan.Username), + "dbName": resources_core.FormatString(plan.DbName), + "role": resources_core.FormatString(plan.Role), + }) + + idParams := map[string]string{ + "username": resources_core.FormatString(plan.Username), + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "modify_user", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "user", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *MongodbUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state MongodbUserModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "delete_user" == "" { + return + } + + instanceUID := strings.TrimSpace(state.MongodbID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{ + "username": resources_core.FormatString(state.Username), + } + identityCodes := []string{ + "username", + } + listKey, idKey := resources_core.ResolveSubresourceStateKeys("user", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{ + "username": resources_core.FormatString(state.Username), + }) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "delete_user", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *MongodbUserResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/93_rabbitmq_resource.go b/generated/test/go/93_rabbitmq_resource.go new file mode 100644 index 0000000..a6dbba5 --- /dev/null +++ b/generated/test/go/93_rabbitmq_resource.go @@ -0,0 +1,462 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "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" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: rabbitmq +// Service ID: 93 + +var _ resource.Resource = &RabbitmqResource{} +var _ resource.ResourceWithModifyPlan = &RabbitmqResource{} +var _ resource.ResourceWithImportState = &RabbitmqResource{} + +type RabbitmqResource struct { + client *core.UniversalClient +} + +type RabbitmqModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + IpSpaceNameMaster types.String `tfsdk:"ip_space_name_master"` + NeedExternalAddressMaster types.Bool `tfsdk:"need_external_address_master"` + ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"` + ResourceDisk types.Int64 `tfsdk:"resource_disk"` + ResourceInstances types.Int64 `tfsdk:"resource_instances"` + ResourceMemory types.Int64 `tfsdk:"resource_memory"` + ResourceRealm types.String `tfsdk:"resource_realm"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewRabbitmqResource() resource.Resource { + return &RabbitmqResource{} +} + +func (r *RabbitmqResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_rabbitmq" +} + +func (r *RabbitmqResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "ip_space_name_master": schema.StringAttribute{Optional: true, MarkdownDescription: "Пример: `internet-no-antiddos-v1`"}, + "need_external_address_master": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false), MarkdownDescription: "Флаг, определяющий, нужно ли выделять внешний IP для доступа к master. Пример: `true` или `false`"}, + "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1000), MarkdownDescription: "Пример: `2000`"}, + "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(30), MarkdownDescription: "Пример: `20`"}, + "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Пример: `1`"}, + "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1124), MarkdownDescription: "Пример: `2048`"}, + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Выбрать платформу из списка Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *RabbitmqResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *RabbitmqModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *RabbitmqModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan RabbitmqModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.ResourceRealm.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + return + } + if config.ResourceRealm.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 116: resources_core.FormatInt64(config.ResourceCPU), + 117: resources_core.FormatInt64(config.ResourceMemory), + 118: resources_core.FormatInt64(config.ResourceDisk), + 119: resources_core.FormatString(config.ResourceRealm), + 224: resources_core.FormatInt64(config.ResourceInstances), + 225: resources_core.FormatBool(config.NeedExternalAddressMaster), + 543: resources_core.FormatString(config.IpSpaceNameMaster), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 93, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *RabbitmqResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data RabbitmqModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 93, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 116: resources_core.FormatInt64(data.ResourceCPU), + 117: resources_core.FormatInt64(data.ResourceMemory), + 118: resources_core.FormatInt64(data.ResourceDisk), + 119: resources_core.FormatString(data.ResourceRealm), + 224: resources_core.FormatInt64(data.ResourceInstances), + 225: resources_core.FormatBool(data.NeedExternalAddressMaster), + 543: resources_core.FormatString(data.IpSpaceNameMaster), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 93, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 93, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "ipSpaceNameMaster", Field: "IpSpaceNameMaster", Type: "string"}, + {Code: "needExternalAddressMaster", Field: "NeedExternalAddressMaster", Type: "bool"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *RabbitmqResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state RabbitmqModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 93, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "ipSpaceNameMaster", Field: "IpSpaceNameMaster", Type: "string"}, + {Code: "needExternalAddressMaster", Field: "NeedExternalAddressMaster", Type: "bool"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *RabbitmqResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan RabbitmqModel + var state RabbitmqModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.ResourceCPU.IsNull() != state.ResourceCPU.IsNull() || plan.ResourceCPU.IsUnknown() != state.ResourceCPU.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() { + if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceMemory.IsNull() != state.ResourceMemory.IsNull() || plan.ResourceMemory.IsUnknown() != state.ResourceMemory.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() { + if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceDisk.IsNull() != state.ResourceDisk.IsNull() || plan.ResourceDisk.IsUnknown() != state.ResourceDisk.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() { + if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceInstances.IsNull() != state.ResourceInstances.IsNull() || plan.ResourceInstances.IsUnknown() != state.ResourceInstances.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() { + if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.NeedExternalAddressMaster.IsNull() != state.NeedExternalAddressMaster.IsNull() || plan.NeedExternalAddressMaster.IsUnknown() != state.NeedExternalAddressMaster.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.NeedExternalAddressMaster.IsNull() && !plan.NeedExternalAddressMaster.IsUnknown() && !state.NeedExternalAddressMaster.IsNull() && !state.NeedExternalAddressMaster.IsUnknown() { + if plan.NeedExternalAddressMaster.ValueBool() != state.NeedExternalAddressMaster.ValueBool() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.IpSpaceNameMaster.IsNull() != state.IpSpaceNameMaster.IsNull() || plan.IpSpaceNameMaster.IsUnknown() != state.IpSpaceNameMaster.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.IpSpaceNameMaster.IsNull() && !plan.IpSpaceNameMaster.IsUnknown() && !state.IpSpaceNameMaster.IsNull() && !state.IpSpaceNameMaster.IsUnknown() { + if !strings.EqualFold(plan.IpSpaceNameMaster.ValueString(), state.IpSpaceNameMaster.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 304: resources_core.FormatInt64(plan.ResourceCPU), + 305: resources_core.FormatInt64(plan.ResourceMemory), + 306: resources_core.FormatInt64(plan.ResourceDisk), + 307: resources_core.FormatInt64(plan.ResourceInstances), + 308: resources_core.FormatBool(plan.NeedExternalAddressMaster), + 545: resources_core.FormatString(plan.IpSpaceNameMaster), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 93, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "ipSpaceNameMaster", Field: "IpSpaceNameMaster", Type: "string"}, + {Code: "needExternalAddressMaster", Field: "NeedExternalAddressMaster", Type: "bool"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *RabbitmqResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state RabbitmqModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *RabbitmqResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *RabbitmqResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/94_lucee_resource.go b/generated/test/go/94_lucee_resource.go new file mode 100644 index 0000000..4f1a93b --- /dev/null +++ b/generated/test/go/94_lucee_resource.go @@ -0,0 +1,449 @@ +package resources_gen + +import ( + "context" + "fmt" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: lucee +// Service ID: 94 + +var _ resource.Resource = &LuceeResource{} +var _ resource.ResourceWithModifyPlan = &LuceeResource{} +var _ resource.ResourceWithImportState = &LuceeResource{} + +type LuceeResource struct { + client *core.UniversalClient +} + +// LuceeAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration. +type LuceeAccessConfigurationModel struct { + Domain types.String `tfsdk:"domain" json:"domain"` +} + +// LuceeAppConfigurationModel — вложенная модель для map-fixed параметра appConfiguration. +type LuceeAppConfigurationModel struct { + GitPath types.String `tfsdk:"git_path" json:"gitPath"` + HealthPath types.String `tfsdk:"health_path" json:"healthPath"` + Version types.String `tfsdk:"version" json:"version"` +} + +// LuceeClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type LuceeClusterConfigurationModel struct { + Cpu types.Int64 `tfsdk:"cpu" json:"cpu"` + Memory types.Int64 `tfsdk:"memory" json:"memory"` + Replicas types.Int64 `tfsdk:"replicas" json:"replicas"` +} + +// LuceeStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type LuceeStartupConfigurationModel struct { + ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"` +} + +type LuceeModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + // --- Redeploy support (ARCHITECTURE.md) --- + // git_revision: при изменении вызывает redeploy вместо modify. + // Опциональное поле — если не задано, modify работает как обычно. + GitRevision types.String `tfsdk:"git_revision"` + AccessConfiguration *LuceeAccessConfigurationModel `tfsdk:"access_configuration"` + AppConfiguration *LuceeAppConfigurationModel `tfsdk:"app_configuration"` + ClusterConfiguration *LuceeClusterConfigurationModel `tfsdk:"cluster_configuration"` + JsonEnv types.String `tfsdk:"json_env"` + StartupConfiguration *LuceeStartupConfigurationModel `tfsdk:"startup_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewLuceeResource() resource.Resource { + return &LuceeResource{} +} + +func (r *LuceeResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_lucee" +} + +func (r *LuceeResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("lucee-k7rjv4db"), MarkdownDescription: "Если указан FQDN в формате: `myapp.example.com`, предварительно будет проверяться DNS-запись (А-запись **должна смотреть на IP Ingress кластера ресурсной платформы**)

Если указан в формате `myapp`, DNS-запись будет автоматически создана в служебной DNS-зоне"}, + }}, + "app_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "git_path": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("https://github.com/xahys/testlucee"), MarkdownDescription: "Путь до репозитория приложения
Обязательно должен быть передан как URL: `https://....`

Должен быть сетевой доступ до URL для скачивания исходников"}, + "health_path": schema.StringAttribute{Optional: true, MarkdownDescription: "Путь до healtcheck для запуска пода в Kubernetes, который отдаст 200 код
Пустое значение означает, что будет проверяться исключительно доступность приложения по порту"}, + "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("6.0")}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(200), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"}, + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(256), MarkdownDescription: "Указывается в Megabytes"}, + "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)}, + }}, + "json_env": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("{}"), MarkdownDescription: "Передаётся в формате КЛЮЧ:ЗНАЧЕНИЕ"}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"}, + }}, + // --- Redeploy support (ARCHITECTURE.md) --- + // При изменении git_revision вызывается redeploy вместо modify. + // Если не задано — modify работает без изменений. + "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *LuceeResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *LuceeModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *LuceeModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan LuceeModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 1019: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}), + 1020: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64())}), + 1021: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", config.AccessConfiguration.Domain.ValueString())}), + 1022: resources_core.BuildJSON(map[string]string{"gitPath": fmt.Sprintf("\"%s\"", config.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", config.AppConfiguration.HealthPath.ValueString()), "version": fmt.Sprintf("\"%s\"", config.AppConfiguration.Version.ValueString())}), + 1023: resources_core.FormatString(config.JsonEnv), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 94, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *LuceeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data LuceeModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 94, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 1019: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}), + 1020: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64())}), + 1021: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", data.AccessConfiguration.Domain.ValueString())}), + 1022: resources_core.BuildJSON(map[string]string{"gitPath": fmt.Sprintf("\"%s\"", data.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", data.AppConfiguration.HealthPath.ValueString()), "version": fmt.Sprintf("\"%s\"", data.AppConfiguration.Version.ValueString())}), + 1023: resources_core.FormatString(data.JsonEnv), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 94, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 94, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "jsonEnv", Field: "JsonEnv", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *LuceeResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state LuceeModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 94, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "jsonEnv", Field: "JsonEnv", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *LuceeResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan LuceeModel + var state LuceeModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.JsonEnv.IsNull() != state.JsonEnv.IsNull() || plan.JsonEnv.IsUnknown() != state.JsonEnv.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.JsonEnv.IsNull() && !plan.JsonEnv.IsUnknown() && !state.JsonEnv.IsNull() && !state.JsonEnv.IsUnknown() { + if !strings.EqualFold(plan.JsonEnv.ValueString(), state.JsonEnv.ValueString()) { + hasServiceParamChanges = true + } + } + } + // --- Redeploy support (ARCHITECTURE.md) --- + // Проверяем изменился ли git_revision. + // Если да — вызываем redeploy вместо modify. + // git_revision = "" (не задано) → обычный modify. + redeployRequested := false + if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() { + if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() { + redeployRequested = true + } + } + + if !hasServiceParamChanges && !redeployRequested { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + // --- Redeploy support (ARCHITECTURE.md) --- + if redeployRequested { + // Вызываем redeploy — пустой params (операция не требует CFS-параметров) + if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + } else { + params := map[int]string{ + 1024: resources_core.BuildJSON(map[string]string{"replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64()), "cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64())}), + 1025: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", plan.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", plan.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", plan.AppConfiguration.HealthPath.ValueString())}), + 1026: resources_core.FormatString(plan.JsonEnv), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 94, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "jsonEnv", Field: "JsonEnv", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *LuceeResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state LuceeModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *LuceeResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *LuceeResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/95_nodejs_resource.go b/generated/test/go/95_nodejs_resource.go new file mode 100644 index 0000000..8822a79 --- /dev/null +++ b/generated/test/go/95_nodejs_resource.go @@ -0,0 +1,449 @@ +package resources_gen + +import ( + "context" + "fmt" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: nodejs +// Service ID: 95 + +var _ resource.Resource = &NodejsResource{} +var _ resource.ResourceWithModifyPlan = &NodejsResource{} +var _ resource.ResourceWithImportState = &NodejsResource{} + +type NodejsResource struct { + client *core.UniversalClient +} + +// NodejsAccessConfigurationModel — вложенная модель для map-fixed параметра accessConfiguration. +type NodejsAccessConfigurationModel struct { + Domain types.String `tfsdk:"domain" json:"domain"` +} + +// NodejsAppConfigurationModel — вложенная модель для map-fixed параметра appConfiguration. +type NodejsAppConfigurationModel struct { + Version types.String `tfsdk:"version" json:"version"` + GitPath types.String `tfsdk:"git_path" json:"gitPath"` + HealthPath types.String `tfsdk:"health_path" json:"healthPath"` +} + +// NodejsClusterConfigurationModel — вложенная модель для map-fixed параметра clusterConfiguration. +type NodejsClusterConfigurationModel struct { + Cpu types.Int64 `tfsdk:"cpu" json:"cpu"` + Memory types.Int64 `tfsdk:"memory" json:"memory"` + Replicas types.Int64 `tfsdk:"replicas" json:"replicas"` +} + +// NodejsStartupConfigurationModel — вложенная модель для map-fixed параметра startupConfiguration. +type NodejsStartupConfigurationModel struct { + ResourceRealm types.String `tfsdk:"resource_realm" json:"resourceRealm"` +} + +type NodejsModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + // --- Redeploy support (ARCHITECTURE.md) --- + // git_revision: при изменении вызывает redeploy вместо modify. + // Опциональное поле — если не задано, modify работает как обычно. + GitRevision types.String `tfsdk:"git_revision"` + AccessConfiguration *NodejsAccessConfigurationModel `tfsdk:"access_configuration"` + AppConfiguration *NodejsAppConfigurationModel `tfsdk:"app_configuration"` + ClusterConfiguration *NodejsClusterConfigurationModel `tfsdk:"cluster_configuration"` + JsonEnv types.String `tfsdk:"json_env"` + StartupConfiguration *NodejsStartupConfigurationModel `tfsdk:"startup_configuration"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewNodejsResource() resource.Resource { + return &NodejsResource{} +} + +func (r *NodejsResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_nodejs" +} + +func (r *NodejsResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "access_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("node-evwflsbl"), MarkdownDescription: "Если указан FQDN в формате: `myapp.example.com`, предварительно будет проверяться DNS-запись (А-запись **должна смотреть на IP Ingress кластера ресурсной платформы**)

Если указан в формате `myapp`, DNS-запись будет автоматически создана в служебной DNS-зоне"}, + }}, + "app_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "version": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("23")}, + "git_path": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("https://github.com/xahys/testnode.git"), MarkdownDescription: "Путь до репозитория приложения
Обязательно должен быть передан как URL: `https://....`

Должен быть сетевой доступ до URL для скачивания исходников"}, + "health_path": schema.StringAttribute{Optional: true, MarkdownDescription: "Путь до healtcheck для запуска пода в Kubernetes, который отдаст 200 код
Пустое значение означает, что будет проверяться исключительно доступность приложения по порту"}, + }}, + "cluster_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(200), MarkdownDescription: "Указывается в Milicores
`1000` Mili == `1` Ядро"}, + "memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(256), MarkdownDescription: "Указывается в Megabytes"}, + "replicas": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1)}, + }}, + "json_env": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("{}"), MarkdownDescription: "Передаётся в формате КЛЮЧ:ЗНАЧЕНИЕ"}, + "startup_configuration": schema.SingleNestedAttribute{Required: true, + Attributes: map[string]schema.Attribute{ + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Кластер Kubernetes, на котором будет развернут экземпляр"}, + }}, + // --- Redeploy support (ARCHITECTURE.md) --- + // При изменении git_revision вызывается redeploy вместо modify. + // Если не задано — modify работает без изменений. + "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *NodejsResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *NodejsModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *NodejsModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan NodejsModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 1028: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", config.StartupConfiguration.ResourceRealm.ValueString())}), + 1029: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", config.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", config.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", config.ClusterConfiguration.Replicas.ValueInt64())}), + 1030: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", config.AccessConfiguration.Domain.ValueString())}), + 1031: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", config.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", config.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", config.AppConfiguration.HealthPath.ValueString())}), + 1032: resources_core.FormatString(config.JsonEnv), + } + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 95, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *NodejsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data NodejsModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 95, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 1028: resources_core.BuildJSON(map[string]string{"resourceRealm": fmt.Sprintf("\"%s\"", data.StartupConfiguration.ResourceRealm.ValueString())}), + 1029: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", data.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", data.ClusterConfiguration.Replicas.ValueInt64())}), + 1030: resources_core.BuildJSON(map[string]string{"domain": fmt.Sprintf("\"%s\"", data.AccessConfiguration.Domain.ValueString())}), + 1031: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", data.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", data.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", data.AppConfiguration.HealthPath.ValueString())}), + 1032: resources_core.FormatString(data.JsonEnv), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 95, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 95, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "jsonEnv", Field: "JsonEnv", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *NodejsResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state NodejsModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 95, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "jsonEnv", Field: "JsonEnv", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *NodejsResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan NodejsModel + var state NodejsModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.JsonEnv.IsNull() != state.JsonEnv.IsNull() || plan.JsonEnv.IsUnknown() != state.JsonEnv.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.JsonEnv.IsNull() && !plan.JsonEnv.IsUnknown() && !state.JsonEnv.IsNull() && !state.JsonEnv.IsUnknown() { + if !strings.EqualFold(plan.JsonEnv.ValueString(), state.JsonEnv.ValueString()) { + hasServiceParamChanges = true + } + } + } + // --- Redeploy support (ARCHITECTURE.md) --- + // Проверяем изменился ли git_revision. + // Если да — вызываем redeploy вместо modify. + // git_revision = "" (не задано) → обычный modify. + redeployRequested := false + if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() { + if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() { + redeployRequested = true + } + } + + if !hasServiceParamChanges && !redeployRequested { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + // --- Redeploy support (ARCHITECTURE.md) --- + if redeployRequested { + // Вызываем redeploy — пустой params (операция не требует CFS-параметров) + if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + } else { + params := map[int]string{ + 1033: resources_core.BuildJSON(map[string]string{"cpu": fmt.Sprintf("%d", plan.ClusterConfiguration.Cpu.ValueInt64()), "memory": fmt.Sprintf("%d", plan.ClusterConfiguration.Memory.ValueInt64()), "replicas": fmt.Sprintf("%d", plan.ClusterConfiguration.Replicas.ValueInt64())}), + 1034: resources_core.BuildJSON(map[string]string{"version": fmt.Sprintf("\"%s\"", plan.AppConfiguration.Version.ValueString()), "gitPath": fmt.Sprintf("\"%s\"", plan.AppConfiguration.GitPath.ValueString()), "healthPath": fmt.Sprintf("\"%s\"", plan.AppConfiguration.HealthPath.ValueString())}), + 1035: resources_core.FormatString(plan.JsonEnv), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 95, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "accessConfiguration", Field: "AccessConfiguration", Type: "map-fixed"}, + {Code: "appConfiguration", Field: "AppConfiguration", Type: "map-fixed"}, + {Code: "clusterConfiguration", Field: "ClusterConfiguration", Type: "map-fixed"}, + {Code: "jsonEnv", Field: "JsonEnv", Type: "string"}, + {Code: "startupConfiguration", Field: "StartupConfiguration", Type: "map-fixed"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *NodejsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state NodejsModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *NodejsResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *NodejsResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/96_pgadmin_resource.go b/generated/test/go/96_pgadmin_resource.go new file mode 100644 index 0000000..661b70b --- /dev/null +++ b/generated/test/go/96_pgadmin_resource.go @@ -0,0 +1,515 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: pgadmin +// Service ID: 96 + +var _ resource.Resource = &PgadminResource{} +var _ resource.ResourceWithModifyPlan = &PgadminResource{} +var _ resource.ResourceWithImportState = &PgadminResource{} + +type PgadminResource struct { + client *core.UniversalClient +} + +type PgadminModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + // --- Redeploy support (ARCHITECTURE.md) --- + // git_revision: при изменении вызывает redeploy вместо modify. + // Опциональное поле — если не задано, modify работает как обычно. + GitRevision types.String `tfsdk:"git_revision"` + Domain types.String `tfsdk:"domain"` + Login types.String `tfsdk:"login"` + Password types.String `tfsdk:"password"` + ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"` + ResourceDisk types.Int64 `tfsdk:"resource_disk"` + ResourceMemory types.Int64 `tfsdk:"resource_memory"` + ResourceRealm types.String `tfsdk:"resource_realm"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewPgadminResource() resource.Resource { + return &PgadminResource{} +} + +func (r *PgadminResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_pgadmin" +} + +func (r *PgadminResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("pgadmin-1"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."}, + "login": schema.StringAttribute{Required: true, MarkdownDescription: "Логин для подключения к pgAdmin, в формате email (login@email.net) Не изменяется после создания."}, + "password": schema.StringAttribute{Required: true, MarkdownDescription: "Пароль для подключения к pgAdmin Не изменяется после создания.", Sensitive: true}, + "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Квота ядра пода (milicores). 1000Mili = 1 Ядро"}, + "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Квота диска (Указывается в GIGAbytes)"}, + "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Квота памяти пода (MBytes)"}, + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + // --- Redeploy support (ARCHITECTURE.md) --- + // При изменении git_revision вызывается redeploy вместо modify. + // Если не задано — modify работает без изменений. + "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *PgadminResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *PgadminModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *PgadminModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan PgadminModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.Login.IsNull() && !plan.Login.IsUnknown() && !state.Login.IsNull() && !state.Login.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Login.ValueString(), state.Login.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить login", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.Password.IsNull() && !plan.Password.IsUnknown() && !state.Password.IsNull() && !state.Password.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Password.ValueString(), state.Password.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить password", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.Login.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "login is required.") + return + } + if config.Login.IsUnknown() { + return + } + if config.Password.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "password is required.") + return + } + if config.Password.IsUnknown() { + return + } + if config.ResourceRealm.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + return + } + if config.ResourceRealm.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 164: resources_core.FormatString(config.Domain), + 165: resources_core.FormatInt64(config.ResourceCPU), + 166: resources_core.FormatInt64(config.ResourceMemory), + 167: resources_core.FormatInt64(config.ResourceDisk), + 169: resources_core.FormatString(config.ResourceRealm), + 170: resources_core.FormatString(config.Login), + 171: resources_core.FormatString(config.Password), + } + desiredDomain := "" + if !config.Domain.IsNull() && !config.Domain.IsUnknown() { + desiredDomain = config.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 96, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *PgadminResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data PgadminModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.Login.IsNull() || data.Login.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "login is required.") + return + } + if data.Password.IsNull() || data.Password.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "password is required.") + return + } + if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + if !data.Domain.IsNull() && !data.Domain.IsUnknown() { + desiredDomain = data.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 96, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 164: resources_core.FormatString(data.Domain), + 165: resources_core.FormatInt64(data.ResourceCPU), + 166: resources_core.FormatInt64(data.ResourceMemory), + 167: resources_core.FormatInt64(data.ResourceDisk), + 169: resources_core.FormatString(data.ResourceRealm), + 170: resources_core.FormatString(data.Login), + 171: resources_core.FormatString(data.Password), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 96, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 96, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "login", Field: "Login", Type: "string"}, + {Code: "password", Field: "Password", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *PgadminResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state PgadminModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 96, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "login", Field: "Login", Type: "string"}, + {Code: "password", Field: "Password", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *PgadminResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan PgadminModel + var state PgadminModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.ResourceCPU.IsNull() != state.ResourceCPU.IsNull() || plan.ResourceCPU.IsUnknown() != state.ResourceCPU.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() { + if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceMemory.IsNull() != state.ResourceMemory.IsNull() || plan.ResourceMemory.IsUnknown() != state.ResourceMemory.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() { + if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceDisk.IsNull() != state.ResourceDisk.IsNull() || plan.ResourceDisk.IsUnknown() != state.ResourceDisk.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() { + if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() { + hasServiceParamChanges = true + } + } + } + // --- Redeploy support (ARCHITECTURE.md) --- + // Проверяем изменился ли git_revision. + // Если да — вызываем redeploy вместо modify. + // git_revision = "" (не задано) → обычный modify. + redeployRequested := false + if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() { + if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() { + redeployRequested = true + } + } + + if !hasServiceParamChanges && !redeployRequested { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + // --- Redeploy support (ARCHITECTURE.md) --- + if redeployRequested { + // Вызываем redeploy — пустой params (операция не требует CFS-параметров) + if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + } else { + params := map[int]string{ + 172: resources_core.FormatInt64(plan.ResourceCPU), + 173: resources_core.FormatInt64(plan.ResourceMemory), + 174: resources_core.FormatInt64(plan.ResourceDisk), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 96, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "login", Field: "Login", Type: "string"}, + {Code: "password", Field: "Password", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *PgadminResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state PgadminModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *PgadminResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *PgadminResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/97_nodered_resource.go b/generated/test/go/97_nodered_resource.go new file mode 100644 index 0000000..f8b17d5 --- /dev/null +++ b/generated/test/go/97_nodered_resource.go @@ -0,0 +1,431 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: nodered +// Service ID: 97 + +var _ resource.Resource = &NoderedResource{} +var _ resource.ResourceWithModifyPlan = &NoderedResource{} +var _ resource.ResourceWithImportState = &NoderedResource{} + +type NoderedResource struct { + client *core.UniversalClient +} + +type NoderedModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + Domain types.String `tfsdk:"domain"` + ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"` + ResourceDisk types.Int64 `tfsdk:"resource_disk"` + ResourceInstances types.Int64 `tfsdk:"resource_instances"` + ResourceMemory types.Int64 `tfsdk:"resource_memory"` + ResourceRealm types.String `tfsdk:"resource_realm"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewNoderedResource() resource.Resource { + return &NoderedResource{} +} + +func (r *NoderedResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_nodered" +} + +func (r *NoderedResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("nodered-3"), MarkdownDescription: "Наименование поддомена, будет добавлено в k8stemplate.dev.nubes.ru Не изменяется после создания."}, + "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Квота ядра пода (Указывается в mili). 1000Mili = 1 Ядро Не изменяется после создания."}, + "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Квота диска (Указывается в GIGAbytes) Не изменяется после создания."}, + "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Количество узлов Не изменяется после создания."}, + "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Квота памяти пода (Указывается в Megabytes) Не изменяется после создания."}, + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *NoderedResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *NoderedModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *NoderedModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan NoderedModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() { + if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_c_p_u", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() { + if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_disk", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() { + if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_instances", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() { + if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() { + resp.Diagnostics.AddError("Нельзя изменить resource_memory", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.ResourceRealm.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + return + } + if config.ResourceRealm.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 193: resources_core.FormatInt64(config.ResourceCPU), + 194: resources_core.FormatInt64(config.ResourceMemory), + 195: resources_core.FormatInt64(config.ResourceDisk), + 196: resources_core.FormatString(config.ResourceRealm), + 197: resources_core.FormatString(config.Domain), + 323: resources_core.FormatInt64(config.ResourceInstances), + } + desiredDomain := "" + if !config.Domain.IsNull() && !config.Domain.IsUnknown() { + desiredDomain = config.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 97, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *NoderedResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data NoderedModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + if !data.Domain.IsNull() && !data.Domain.IsUnknown() { + desiredDomain = data.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 97, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 193: resources_core.FormatInt64(data.ResourceCPU), + 194: resources_core.FormatInt64(data.ResourceMemory), + 195: resources_core.FormatInt64(data.ResourceDisk), + 196: resources_core.FormatString(data.ResourceRealm), + 197: resources_core.FormatString(data.Domain), + 323: resources_core.FormatInt64(data.ResourceInstances), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 97, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 97, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *NoderedResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state NoderedModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 97, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *NoderedResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan NoderedModel + var state NoderedModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{} + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 97, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *NoderedResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state NoderedModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "delete" + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *NoderedResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *NoderedResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/98_http_resource.go b/generated/test/go/98_http_resource.go new file mode 100644 index 0000000..5fe21d9 --- /dev/null +++ b/generated/test/go/98_http_resource.go @@ -0,0 +1,509 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: http +// Service ID: 98 + +var _ resource.Resource = &HttpResource{} +var _ resource.ResourceWithModifyPlan = &HttpResource{} +var _ resource.ResourceWithImportState = &HttpResource{} + +type HttpResource struct { + client *core.UniversalClient +} + +type HttpModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + // --- Redeploy support (ARCHITECTURE.md) --- + // git_revision: при изменении вызывает redeploy вместо modify. + // Опциональное поле — если не задано, modify работает как обычно. + GitRevision types.String `tfsdk:"git_revision"` + Domain types.String `tfsdk:"domain"` + IngressAnnotations types.String `tfsdk:"ingress_annotations"` + JsonEnv types.String `tfsdk:"json_env"` + RegistryPath types.String `tfsdk:"registry_path"` + ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"` + ResourceInstances types.Int64 `tfsdk:"resource_instances"` + ResourceMemory types.Int64 `tfsdk:"resource_memory"` + ResourceRealm types.String `tfsdk:"resource_realm"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewHttpResource() resource.Resource { + return &HttpResource{} +} + +func (r *HttpResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_http" +} + +func (r *HttpResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("http-1"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."}, + "ingress_annotations": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("{}"), MarkdownDescription: "Формат ключ/значение
Поддерживает пока простые аннотации Не изменяется после создания."}, + "json_env": schema.StringAttribute{Optional: true, MarkdownDescription: "Массив переменных, будут переданы в env приложения Не изменяется после создания."}, + "registry_path": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("nexus-sa.tst.nubes.ru/docker-nubes/jolt"), MarkdownDescription: "Путь до image, который будет установлен как deployment Не изменяется после создания."}, + "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(100), MarkdownDescription: "Квота ядра пода (Указывается в mili)"}, + "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Количество узлов"}, + "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(200), MarkdownDescription: "Квота памяти пода (Указывается в Megabytes)"}, + "resource_realm": schema.StringAttribute{Required: true, MarkdownDescription: "Платформа для развертывания Не изменяется после создания."}, + // --- Redeploy support (ARCHITECTURE.md) --- + // При изменении git_revision вызывается redeploy вместо modify. + // Если не задано — modify работает без изменений. + "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *HttpResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *HttpModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *HttpModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan HttpModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.IngressAnnotations.IsNull() && !plan.IngressAnnotations.IsUnknown() && !state.IngressAnnotations.IsNull() && !state.IngressAnnotations.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.IngressAnnotations.ValueString(), state.IngressAnnotations.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить ingress_annotations", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.JsonEnv.IsNull() && !plan.JsonEnv.IsUnknown() && !state.JsonEnv.IsNull() && !state.JsonEnv.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.JsonEnv.ValueString(), state.JsonEnv.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить json_env", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.RegistryPath.IsNull() && !plan.RegistryPath.IsUnknown() && !state.RegistryPath.IsNull() && !state.RegistryPath.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.RegistryPath.ValueString(), state.RegistryPath.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить registry_path", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.ResourceRealm.IsNull() && !plan.ResourceRealm.IsUnknown() && !state.ResourceRealm.IsNull() && !state.ResourceRealm.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.ResourceRealm.ValueString(), state.ResourceRealm.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить resource_realm", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.ResourceRealm.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + return + } + if config.ResourceRealm.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + params := map[int]string{ + 212: resources_core.FormatString(config.Domain), + 213: resources_core.FormatInt64(config.ResourceCPU), + 214: resources_core.FormatInt64(config.ResourceMemory), + 215: resources_core.FormatInt64(config.ResourceInstances), + 216: resources_core.FormatString(config.ResourceRealm), + 217: resources_core.FormatString(config.RegistryPath), + 241: resources_core.FormatString(config.JsonEnv), + 782: resources_core.FormatString(config.IngressAnnotations), + } + desiredDomain := "" + if !config.Domain.IsNull() && !config.Domain.IsUnknown() { + desiredDomain = config.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 98, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *HttpResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data HttpModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "resource_realm is required.") + return + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + if !data.Domain.IsNull() && !data.Domain.IsUnknown() { + desiredDomain = data.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 98, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 212: resources_core.FormatString(data.Domain), + 213: resources_core.FormatInt64(data.ResourceCPU), + 214: resources_core.FormatInt64(data.ResourceMemory), + 215: resources_core.FormatInt64(data.ResourceInstances), + 216: resources_core.FormatString(data.ResourceRealm), + 217: resources_core.FormatString(data.RegistryPath), + 241: resources_core.FormatString(data.JsonEnv), + 782: resources_core.FormatString(data.IngressAnnotations), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 98, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 98, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "ingressAnnotations", Field: "IngressAnnotations", Type: "string"}, + {Code: "jsonEnv", Field: "JsonEnv", Type: "string"}, + {Code: "registryPath", Field: "RegistryPath", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *HttpResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state HttpModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 98, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "ingressAnnotations", Field: "IngressAnnotations", Type: "string"}, + {Code: "jsonEnv", Field: "JsonEnv", Type: "string"}, + {Code: "registryPath", Field: "RegistryPath", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *HttpResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan HttpModel + var state HttpModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.ResourceCPU.IsNull() != state.ResourceCPU.IsNull() || plan.ResourceCPU.IsUnknown() != state.ResourceCPU.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() { + if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceMemory.IsNull() != state.ResourceMemory.IsNull() || plan.ResourceMemory.IsUnknown() != state.ResourceMemory.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() { + if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceInstances.IsNull() != state.ResourceInstances.IsNull() || plan.ResourceInstances.IsUnknown() != state.ResourceInstances.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() { + if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() { + hasServiceParamChanges = true + } + } + } + // --- Redeploy support (ARCHITECTURE.md) --- + // Проверяем изменился ли git_revision. + // Если да — вызываем redeploy вместо modify. + // git_revision = "" (не задано) → обычный modify. + redeployRequested := false + if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() { + if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() { + redeployRequested = true + } + } + + if !hasServiceParamChanges && !redeployRequested { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + // --- Redeploy support (ARCHITECTURE.md) --- + if redeployRequested { + // Вызываем redeploy — пустой params (операция не требует CFS-параметров) + if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + } else { + params := map[int]string{ + 218: resources_core.FormatInt64(plan.ResourceCPU), + 219: resources_core.FormatInt64(plan.ResourceMemory), + 220: resources_core.FormatInt64(plan.ResourceInstances), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 98, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "ingressAnnotations", Field: "IngressAnnotations", Type: "string"}, + {Code: "jsonEnv", Field: "JsonEnv", Type: "string"}, + {Code: "registryPath", Field: "RegistryPath", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + {Code: "resourceRealm", Field: "ResourceRealm", Type: "string"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *HttpResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state HttpModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *HttpResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *HttpResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/99_gitea_resource.go b/generated/test/go/99_gitea_resource.go new file mode 100644 index 0000000..0e8a084 --- /dev/null +++ b/generated/test/go/99_gitea_resource.go @@ -0,0 +1,579 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: gitea +// Service ID: 99 + +var _ resource.Resource = &GiteaResource{} +var _ resource.ResourceWithModifyPlan = &GiteaResource{} +var _ resource.ResourceWithImportState = &GiteaResource{} + +type GiteaResource struct { + client *core.UniversalClient +} + +type GiteaModel struct { + ID types.String `tfsdk:"id"` + ResourceName types.String `tfsdk:"resource_name"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` + Domain types.String `tfsdk:"domain"` + IngressExtraAnnotations types.String `tfsdk:"ingress_extra_annotations"` + IniServerParams types.String `tfsdk:"ini_server_params"` + PsqlUid types.String `tfsdk:"psql_uid"` + ResourceCPU types.Int64 `tfsdk:"resource_c_p_u"` + ResourceDisk types.Int64 `tfsdk:"resource_disk"` + ResourceInstances types.Int64 `tfsdk:"resource_instances"` + ResourceMemory types.Int64 `tfsdk:"resource_memory"` + SuspendOnDestroy types.Bool `tfsdk:"suspend_on_destroy"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + StateParams types.Map `tfsdk:"state_params"` + StateOut types.Map `tfsdk:"state_out"` + StateParamsFlat types.Map `tfsdk:"state_params_flat"` + StateOutFlat types.Map `tfsdk:"state_out_flat"` + VaultSecrets types.Map `tfsdk:"vault_secrets"` + VaultUrl types.String `tfsdk:"vault_url"` + VaultUserPath types.String `tfsdk:"vault_user_path"` + VaultFields types.List `tfsdk:"vault_fields"` +} + +func NewGiteaResource() resource.Resource { + return &GiteaResource{} +} + +func (r *GiteaResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_gitea" +} + +func (r *GiteaResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "resource_name": schema.StringAttribute{Required: true}, + "operation_timeout": schema.StringAttribute{Optional: true}, + "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."}, + "domain": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("gitea-1"), MarkdownDescription: "Если будет указан FQDN, то происходят проверки DNS на валидность А-записи, или будет добавлено в зону resourceRealm Не изменяется после создания."}, + "ingress_extra_annotations": schema.StringAttribute{Optional: true}, + "ini_server_params": schema.StringAttribute{Optional: true}, + "psql_uid": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "UUID Услуги k8sPgsql. Пользователя techGitea, базу данных gitea создаёт автоматически Не изменяется после создания."}, + "resource_c_p_u": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500), MarkdownDescription: "Квота ядра пода (Указывается в mili). 1000Mili = 1 Ядро"}, + "resource_disk": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Квота диска (Указывается в GIGAbytes)"}, + "resource_instances": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(1), MarkdownDescription: "Количество узлов"}, + "resource_memory": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(512), MarkdownDescription: "Квота памяти пода (Указывается в Megabytes)"}, + "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "state_params": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_params_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "state_out_flat": schema.MapAttribute{Computed: true, ElementType: types.StringType}, + "vault_secrets": schema.MapAttribute{Computed: true, ElementType: types.StringType, Sensitive: true}, + "vault_url": schema.StringAttribute{Computed: true}, + "vault_user_path": schema.StringAttribute{Computed: true}, + "vault_fields": schema.ListAttribute{Computed: true, ElementType: types.StringType}, + } + + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *GiteaResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.client == nil { + return + } + + var config *GiteaModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + if config == nil { + return + } + + var state *GiteaModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() { + return + } + + if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() { + var plan GiteaModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan. + // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов. + // Resolve (displayName → UUID или uppercase → lowercase) нужен только для + // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка + // "Provider produced invalid plan: planned value does not match config value". + if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() { + if plan.ResourceName.ValueString() != state.ResourceName.ValueString() { + resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.") + return + } + } + if !plan.Domain.IsNull() && !plan.Domain.IsUnknown() && !state.Domain.IsNull() && !state.Domain.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.Domain.ValueString(), state.Domain.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить domain", "Параметр задается при создании и не может быть изменен.") + return + } + } + if !plan.PsqlUid.IsNull() && !plan.PsqlUid.IsUnknown() && !state.PsqlUid.IsNull() && !state.PsqlUid.IsUnknown() { + // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID + // в lowercase, пользователь мог написать upper/mixed. Это одно и то же + // значение, менять его нельзя только если оно реально другое. + if !strings.EqualFold(plan.PsqlUid.ValueString(), state.PsqlUid.ValueString()) { + resp.Diagnostics.AddError("Нельзя изменить psql_uid", "Параметр задается при создании и не может быть изменен.") + return + } + } + return + } + if config.PsqlUid.IsNull() { + resp.Diagnostics.AddError("Missing required attribute", "psql_uid is required.") + return + } + if config.PsqlUid.IsUnknown() { + return + } + + if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() { + return + } + adoptExistingOnCreate := false + if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() { + adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool() + } + if !config.PsqlUid.IsNull() && !config.PsqlUid.IsUnknown() { + resolvedPsqlUid, err := r.client.ResolveRefSvcParamValue(ctx, 90, config.PsqlUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve psql_uid", err.Error()) + } else if resolvedPsqlUid != "" && resolvedPsqlUid != config.PsqlUid.ValueString() { + config.PsqlUid = types.StringValue(resolvedPsqlUid) + } + } + params := map[int]string{ + 232: resources_core.FormatInt64(config.ResourceCPU), + 233: resources_core.FormatInt64(config.ResourceMemory), + 234: resources_core.FormatInt64(config.ResourceDisk), + 235: resources_core.FormatInt64(config.ResourceInstances), + 237: resources_core.FormatString(config.Domain), + 238: resources_core.FormatString(config.PsqlUid), + 818: resources_core.FormatString(config.IngressExtraAnnotations), + 821: resources_core.FormatString(config.IniServerParams), + } + desiredDomain := "" + if !config.Domain.IsNull() && !config.Domain.IsUnknown() { + desiredDomain = config.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + + resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, 99, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...) +} + +func (r *GiteaResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data GiteaModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + if data.PsqlUid.IsNull() || data.PsqlUid.IsUnknown() { + resp.Diagnostics.AddError("Missing required attribute", "psql_uid is required.") + return + } + // ══════════════════════════════════════════════════════════════════════════ + // ПРАВИЛО TERRAFORM (официальная документация): + // «If an attribute value is configured, it is NEVER valid to change that + // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что + // написал пользователь). Менять plan запрещено на уровне фреймворка. + // + // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре. + // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный). + // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит: + // «Provider produced inconsistent result after apply». + // + // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот. + // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как + // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API). + // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный + // регистр в state через strings.EqualFold (сравниваем без учёта регистра). + // ══════════════════════════════════════════════════════════════════════════ + originalPsqlUid := data.PsqlUid + if !data.PsqlUid.IsNull() && !data.PsqlUid.IsUnknown() { + resolvedPsqlUid, err := r.client.ResolveRefSvcParamValue(ctx, 90, data.PsqlUid.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if resolvedPsqlUid != "" && resolvedPsqlUid != data.PsqlUid.ValueString() { + data.PsqlUid = types.StringValue(resolvedPsqlUid) + } + } + + resourceName := data.ResourceName.ValueString() + desiredDomain := "" + if !data.Domain.IsNull() && !data.Domain.IsUnknown() { + desiredDomain = data.Domain.ValueString() + } + domainServiceIDs := []int{81, 82, 88, 96, 97, 98, 99, 119, 149, 151} + resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, 99, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...) + // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt, + // not created, конфликт) сайд-эффект create не должен выполняться. + if resp.Diagnostics.HasError() { + return + } + + params := map[int]string{ + 232: resources_core.FormatInt64(data.ResourceCPU), + 233: resources_core.FormatInt64(data.ResourceMemory), + 234: resources_core.FormatInt64(data.ResourceDisk), + 235: resources_core.FormatInt64(data.ResourceInstances), + 237: resources_core.FormatString(data.Domain), + 238: resources_core.FormatString(data.PsqlUid), + 818: resources_core.FormatString(data.IngressExtraAnnotations), + 821: resources_core.FormatString(data.IniServerParams), + } + + operationTimeout := "" + if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() { + operationTimeout = data.OperationTimeout.ValueString() + } + if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString()) + } + id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, 99, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + data.ID = types.StringValue(id) + + state, diags := resources_core.RefreshResourceState(ctx, r.client, id, 99, data, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "ingressExtraAnnotations", Field: "IngressExtraAnnotations", Type: "string"}, + {Code: "iniServerParams", Field: "IniServerParams", Type: "string"}, + {Code: "psqlUid", Field: "PsqlUid", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.PsqlUid.IsNull() && !state.PsqlUid.IsUnknown() { + resolvedPsqlUid, err := r.client.ResolveRefSvcParamValue(ctx, 90, state.PsqlUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve psql_uid", err.Error()) + } else if resolvedPsqlUid != "" && resolvedPsqlUid != state.PsqlUid.ValueString() { + state.PsqlUid = types.StringValue(resolvedPsqlUid) + } + } + // Restore user-provided casing в state (Create). + // EqualFold = «совпадают ли значения без учёта регистра?» + // Если да — значит API вернул «туже» строку, только в другом регистре. + // Заменяем state на original (то что было в plan/config пользователя). + // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint. + if !originalPsqlUid.IsNull() && !originalPsqlUid.IsUnknown() && !state.PsqlUid.IsNull() && !state.PsqlUid.IsUnknown() { + if strings.EqualFold(state.PsqlUid.ValueString(), originalPsqlUid.ValueString()) { + state.PsqlUid = originalPsqlUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *GiteaResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state GiteaModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + if r.client != nil { + remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if remove { + resp.State.RemoveResource(ctx) + return + } + } + + newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), 99, state, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "ingressExtraAnnotations", Field: "IngressExtraAnnotations", Type: "string"}, + {Code: "iniServerParams", Field: "IniServerParams", Type: "string"}, + {Code: "psqlUid", Field: "PsqlUid", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !newState.PsqlUid.IsNull() && !newState.PsqlUid.IsUnknown() { + resolvedPsqlUid, err := r.client.ResolveRefSvcParamValue(ctx, 90, newState.PsqlUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve psql_uid", err.Error()) + } else if resolvedPsqlUid != "" && resolvedPsqlUid != newState.PsqlUid.ValueString() { + newState.PsqlUid = types.StringValue(resolvedPsqlUid) + } + } + // Restore user-provided casing в state (Read). + // При чтении у нас нет plan — но предыдущий state уже хранит значение + // в регистре пользователя (после первого Create оно было восстановлено). + // Берём prior state (переменная state) как эталон регистра. + // Если API вернул то же UUID только строчными буквами — восстанавливаем. + if !state.PsqlUid.IsNull() && !state.PsqlUid.IsUnknown() && !newState.PsqlUid.IsNull() && !newState.PsqlUid.IsUnknown() { + if strings.EqualFold(newState.PsqlUid.ValueString(), state.PsqlUid.ValueString()) { + newState.PsqlUid = state.PsqlUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) +} + +func (r *GiteaResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan GiteaModel + var state GiteaModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + instanceID := state.ID + if instanceID.IsNull() || instanceID.IsUnknown() { + instanceID = plan.ID + } + if instanceID.IsNull() || instanceID.IsUnknown() { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify") + return + } + + hasServiceParamChanges := false + if !hasServiceParamChanges { + if plan.ResourceCPU.IsNull() != state.ResourceCPU.IsNull() || plan.ResourceCPU.IsUnknown() != state.ResourceCPU.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceCPU.IsNull() && !plan.ResourceCPU.IsUnknown() && !state.ResourceCPU.IsNull() && !state.ResourceCPU.IsUnknown() { + if plan.ResourceCPU.ValueInt64() != state.ResourceCPU.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceMemory.IsNull() != state.ResourceMemory.IsNull() || plan.ResourceMemory.IsUnknown() != state.ResourceMemory.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceMemory.IsNull() && !plan.ResourceMemory.IsUnknown() && !state.ResourceMemory.IsNull() && !state.ResourceMemory.IsUnknown() { + if plan.ResourceMemory.ValueInt64() != state.ResourceMemory.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceDisk.IsNull() != state.ResourceDisk.IsNull() || plan.ResourceDisk.IsUnknown() != state.ResourceDisk.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceDisk.IsNull() && !plan.ResourceDisk.IsUnknown() && !state.ResourceDisk.IsNull() && !state.ResourceDisk.IsUnknown() { + if plan.ResourceDisk.ValueInt64() != state.ResourceDisk.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.ResourceInstances.IsNull() != state.ResourceInstances.IsNull() || plan.ResourceInstances.IsUnknown() != state.ResourceInstances.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.ResourceInstances.IsNull() && !plan.ResourceInstances.IsUnknown() && !state.ResourceInstances.IsNull() && !state.ResourceInstances.IsUnknown() { + if plan.ResourceInstances.ValueInt64() != state.ResourceInstances.ValueInt64() { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.IngressExtraAnnotations.IsNull() != state.IngressExtraAnnotations.IsNull() || plan.IngressExtraAnnotations.IsUnknown() != state.IngressExtraAnnotations.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.IngressExtraAnnotations.IsNull() && !plan.IngressExtraAnnotations.IsUnknown() && !state.IngressExtraAnnotations.IsNull() && !state.IngressExtraAnnotations.IsUnknown() { + if !strings.EqualFold(plan.IngressExtraAnnotations.ValueString(), state.IngressExtraAnnotations.ValueString()) { + hasServiceParamChanges = true + } + } + } + if !hasServiceParamChanges { + if plan.IniServerParams.IsNull() != state.IniServerParams.IsNull() || plan.IniServerParams.IsUnknown() != state.IniServerParams.IsUnknown() { + hasServiceParamChanges = true + } else if !plan.IniServerParams.IsNull() && !plan.IniServerParams.IsUnknown() && !state.IniServerParams.IsNull() && !state.IniServerParams.IsUnknown() { + if !strings.EqualFold(plan.IniServerParams.ValueString(), state.IniServerParams.ValueString()) { + hasServiceParamChanges = true + } + } + } + + if !hasServiceParamChanges { + plan.ID = instanceID + plan.StateParams = state.StateParams + plan.StateOut = state.StateOut + plan.StateParamsFlat = state.StateParamsFlat + plan.StateOutFlat = state.StateOutFlat + plan.VaultSecrets = state.VaultSecrets + plan.VaultUrl = state.VaultUrl + plan.VaultUserPath = state.VaultUserPath + plan.VaultFields = state.VaultFields + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + params := map[int]string{ + 259: resources_core.FormatInt64(plan.ResourceCPU), + 260: resources_core.FormatInt64(plan.ResourceMemory), + 261: resources_core.FormatInt64(plan.ResourceDisk), + 262: resources_core.FormatInt64(plan.ResourceInstances), + 819: resources_core.FormatString(plan.IngressExtraAnnotations), + 820: resources_core.FormatString(plan.IniServerParams), + } + if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = instanceID + state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), 99, plan, []resources_core.StateField{ + {Code: "state_params"}, + {Code: "state_out"}, + {Code: "state_params_flat"}, + {Code: "state_out_flat"}, + {Code: "vault_secrets"}, + {Code: "vault_url"}, + {Code: "vault_user_path"}, + {Code: "vault_fields"}, + }, []resources_core.InputField{ + {Code: "domain", Field: "Domain", Type: "string"}, + {Code: "ingressExtraAnnotations", Field: "IngressExtraAnnotations", Type: "string"}, + {Code: "iniServerParams", Field: "IniServerParams", Type: "string"}, + {Code: "psqlUid", Field: "PsqlUid", Type: "string"}, + {Code: "resourceCPU", Field: "ResourceCPU", Type: "int64"}, + {Code: "resourceDisk", Field: "ResourceDisk", Type: "int64"}, + {Code: "resourceInstances", Field: "ResourceInstances", Type: "int64"}, + {Code: "resourceMemory", Field: "ResourceMemory", Type: "int64"}, + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if !state.PsqlUid.IsNull() && !state.PsqlUid.IsUnknown() { + resolvedPsqlUid, err := r.client.ResolveRefSvcParamValue(ctx, 90, state.PsqlUid.ValueString()) + if err != nil { + resp.Diagnostics.AddWarning("Failed to resolve psql_uid", err.Error()) + } else if resolvedPsqlUid != "" && resolvedPsqlUid != state.PsqlUid.ValueString() { + state.PsqlUid = types.StringValue(resolvedPsqlUid) + } + } + // Restore user-provided casing в state (Update). + // После API-вызова modify state содержит значения в lower-case от API. + // Plan == config == то что написал пользователь (регистр неизменён). + // EqualFold: если UUID совпадает без учёта регистра — берём из plan. + if !plan.PsqlUid.IsNull() && !plan.PsqlUid.IsUnknown() && !state.PsqlUid.IsNull() && !state.PsqlUid.IsUnknown() { + if strings.EqualFold(state.PsqlUid.ValueString(), plan.PsqlUid.ValueString()) { + state.PsqlUid = plan.PsqlUid + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *GiteaResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state GiteaModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if state.ID.IsNull() || state.ID.IsUnknown() { + return + } + deleteMode := "state_only" + if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() { + deleteMode = "suspend" + } + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } +} + +func (r *GiteaResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *GiteaResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/generated/test/go/registry.go b/generated/test/go/registry.go new file mode 100644 index 0000000..e0f02fc --- /dev/null +++ b/generated/test/go/registry.go @@ -0,0 +1,78 @@ +package resources_gen + +import "github.com/hashicorp/terraform-plugin-framework/resource" + +// Code generated by tools/gen_v2. DO NOT EDIT. +func AllResources() []func() resource.Resource { + return []func() resource.Resource{ + NewAkhqResource, + NewClickhouseResource, + NewDnsrecordResource, + NewDnszoneResource, + NewDummyResource, + NewFlaskResource, + NewGiteaResource, + NewGiteaComplexResource, + NewHarborResource, + NewHttpResource, + NewK8sOpenbaoResource, + NewK8sSthutrvalClusterResource, + NewK8sVeleroResource, + NewK8sZitiControllerResource, + NewKafkaResource, + NewLlmAiResource, + NewLuceeResource, + NewMariadbResource, + NewMongodbResource, + NewNextcloudResource, + NewNifiResource, + NewNodejsResource, + NewNoderedResource, + NewOpenwhiskResource, + NewPgadminResource, + NewPostgresResource, + NewRabbitmqResource, + NewRedisResource, + NewS3Resource, + NewS3bucketResource, + NewSupersetResource, + NewTemplateResource, + NewTenantResource, + NewValoTenantResource, + NewVappResource, + NewVcComplexResource, + NewVcMgmtSthutrvalClusterResource, + NewVcNsxtResource, + NewVcOrgResource, + NewVcOrgSaasResource, + NewVcVdcResource, + NewVcVdcGroupResource, + NewVcVmResource, + NewVcVmV2Resource, + NewVcVmV3Resource, + NewVcexternalipResource, + NewVmpostgreResource, + NewZonesV2Resource, + NewClickhouseDatabaseResource, + NewClickhouseUserResource, + NewK8sOpenbaoKvResource, + NewK8sOpenbaoPolicyResource, + NewK8sOpenbaoSecretsResource, + NewK8sSthutrvalClusterUserResource, + NewKafkaTopicResource, + NewKafkaUserResource, + NewLlmAiSecretsResource, + NewLlmAiTeamResource, + NewMariadbDatabaseResource, + NewMariadbUserResource, + NewMongodbUserResource, + NewNextcloudBackupResource, + NewPostgresBackupResource, + NewPostgresDatabaseResource, + NewPostgresUserResource, + NewS3SubUserResource, + NewVcOrgUserResource, + NewVcVdcGroupVdcResource, + NewZonesV2RecordResource, + } +} diff --git a/provider/main.go b/provider/main.go index 56c70be..5a49677 100644 --- a/provider/main.go +++ b/provider/main.go @@ -17,7 +17,7 @@ import ( ) var ( - version string = "5.0.73" + version string = "5.0.74" ) func main() {